From 39fd901ba01527c209cc5322ed2017cb93815711 Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Sun, 5 Mar 2023 23:49:08 +0100 Subject: [PATCH 01/19] minor code changes some minor internal changes --- internal/parser/http1/chunkedbodyparser.go | 10 ++++++++++ internal/parser/http1/requestsparser.go | 4 ++-- internal/server/http/http.go | 7 +++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/internal/parser/http1/chunkedbodyparser.go b/internal/parser/http1/chunkedbodyparser.go index 6a3f4e55..37fb4ac2 100644 --- a/internal/parser/http1/chunkedbodyparser.go +++ b/internal/parser/http1/chunkedbodyparser.go @@ -8,6 +8,10 @@ import ( "github.com/indigo-web/indigo/settings" ) +type ChunkedBodyParser interface { + Parse(data []byte, trailer bool) (chunk, extra []byte, err error) +} + // chunkedBodyParser is a parser for chunked encoded request bodies // used to encapsulate process of parsing because it's more convenient // to leave the process here and let main parser parse only http requests @@ -18,6 +22,12 @@ type chunkedBodyParser struct { chunkLength int } +func NewChunkedBodyParser(settings settings.Body) ChunkedBodyParser { + parser := newChunkedBodyParser(settings) + + return &parser +} + func newChunkedBodyParser(settings settings.Body) chunkedBodyParser { return chunkedBodyParser{ state: eChunkLength1Char, diff --git a/internal/parser/http1/requestsparser.go b/internal/parser/http1/requestsparser.go index 9b27d183..cd06b5c7 100644 --- a/internal/parser/http1/requestsparser.go +++ b/internal/parser/http1/requestsparser.go @@ -391,8 +391,6 @@ proto: switch data[0] { case 'H', 'h': - p.begin = 0 - p.pointer = 0 data = data[1:] p.state = eH goto protoH @@ -823,6 +821,8 @@ func (p *httpRequestsParser) reset() { p.protoMinor = 0 p.headersNumber = 0 p.chunkedTransferEncoding = false + p.begin = 0 + p.pointer = 0 p.headerKeyAllocator.Clear() p.headerValueAllocator.Clear() p.trailer = false diff --git a/internal/server/http/http.go b/internal/server/http/http.go index 217f8898..a8d59e58 100644 --- a/internal/server/http/http.go +++ b/internal/server/http/http.go @@ -4,16 +4,15 @@ import ( "bytes" "errors" "fmt" + "github.com/indigo-web/indigo/http" "github.com/indigo-web/indigo/http/proto" "github.com/indigo-web/indigo/http/status" "github.com/indigo-web/indigo/internal" + "github.com/indigo-web/indigo/internal/parser" "github.com/indigo-web/indigo/internal/render" "github.com/indigo-web/indigo/internal/server/tcp" - "os" - - "github.com/indigo-web/indigo/http" - "github.com/indigo-web/indigo/internal/parser" "github.com/indigo-web/indigo/router" + "os" ) type Server interface { From 2c862c78d1d51eda35765b9b4a1d3e5357f516c7 Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Sun, 5 Mar 2023 23:52:43 +0100 Subject: [PATCH 02/19] added attachments added attachments - io.Reader's that are being memory-efficiently rendered; also made some minor improvements that now allows to include Content-Type header on the level of engine, not by default headers --- http/response.go | 144 ++++++++++++++++++++++++++++++-------- internal/render/engine.go | 120 ++++++++++++++++++++----------- 2 files changed, 194 insertions(+), 70 deletions(-) diff --git a/http/response.go b/http/response.go index 6526ef2f..e44f6213 100644 --- a/http/response.go +++ b/http/response.go @@ -2,19 +2,44 @@ package http import ( "io" + "os" + "strings" "github.com/indigo-web/indigo/http/status" "github.com/indigo-web/indigo/internal" ) -type ( - ResponseWriter func(b []byte) error - Render func(response Response) error - FileErrHandler func(err error) Response -) +type ResponseWriter func(b []byte) error // IDK why 7, but let it be -const defaultHeadersNumber = 7 +const ( + defaultHeadersNumber = 7 + defaultContentType = "text/html" +) + +// Attachment is a wrapper for io.Reader, with the difference that there is the size attribute. +// If positive value (including 0) is set, then ordinary plain-text response will be rendered. +// Otherwise, chunked transfer encoding is used. +type Attachment struct { + content io.Reader + size int +} + +// NewAttachment returns a new Attachment instance +func NewAttachment(content io.Reader, size int) Attachment { + return Attachment{ + content: content, + size: size, + } +} + +func (a Attachment) Content() io.Reader { + return a.content +} + +func (a Attachment) Size() int { + return a.size +} type Response struct { Code status.Code @@ -23,16 +48,24 @@ type Response struct { // headers are just a slice of strings, length of which is always dividable by 2, because // it contains pairs of keys and values headers []string - // Body is a response body byte-slice that contains raw data - Body []byte - filename string - handler FileErrHandler + // ContentType, as a special for core header, should be treated individually + ContentType string + // The same is about TransferEncoding + TransferEncoding string + Body []byte + + // attachment is a reader that's going to be read only at response's rendering, so its + // processing should usually be quite efficient. + // + // Note: if attachment is set, Body will be ignored + attachment Attachment } func NewResponse() Response { return Response{ - Code: status.OK, - headers: make([]string, 0, defaultHeadersNumber*2), + Code: status.OK, + headers: make([]string, 0, defaultHeadersNumber*2), + ContentType: defaultContentType, } } @@ -52,9 +85,28 @@ func (r Response) WithStatus(status status.Status) Response { return r } +// WithContentType sets a custom Content-Type header value. +func (r Response) WithContentType(value string) Response { + r.ContentType = value + return r +} + +// WithTransferEncoding sets a custom Transfer-Encoding header value. +func (r Response) WithTransferEncoding(value string) Response { + r.TransferEncoding = value + return r +} + // WithHeader sets header values to a key. In case it already exists the value will -// be appended +// be appended. func (r Response) WithHeader(key string, values ...string) Response { + switch { + case strings.EqualFold(key, "content-type"): + return r.WithContentType(values[0]) + case strings.EqualFold(key, "transfer-encoding"): + return r.WithTransferEncoding(values[0]) + } + for i := range values { r.headers = append(r.headers, key, values[i]) } @@ -100,6 +152,7 @@ func (r Response) WithBodyByte(body []byte) Response { // WithWriter takes a function that takes an io.Writer, which allows us to stream data // directly into the response body. // Note: this method causes an allocation +// // TODO: This is not the best design solution. I would like to make this method just like // // all others, so returning only Response object itself. The problem is that it is @@ -112,20 +165,29 @@ func (r Response) WithWriter(cb func(io.Writer) error) (Response, error) { return writer.response, err } -// WithFile sets a file path as a file that is supposed to be uploaded as a -// response. File replaces a response body, so in case last one is specified, -// it'll be ignored. -// In case any error occurred (file not found, or error occurred during reading, -// etc.), handler will be called with a raised error -func (r Response) WithFile(path string, handler FileErrHandler) Response { - r.filename = path - r.handler = handler +// WithFile opens a file for reading, and returns a new response with attachment corresponding +// to the file FD. In case not found or any other error, it'll be directly returned +func (r Response) WithFile(path string) (Response, error) { + file, err := os.Open(path) + if err != nil { + return r, err + } + + stat, err := file.Stat() + attachment := NewAttachment(file, int(stat.Size())) + + return r.WithAttachment(attachment), err +} + +// WithAttachment sets a response's attachment. In this case response body will be ignored +func (r Response) WithAttachment(attachment Attachment) Response { + r.attachment = attachment return r } // WithError tries to set a corresponding status code and response body equal to error text // if error is known to server, otherwise setting status code to status.InternalServerError -// without setting a response body to the error text, because this possibly can reveal some +// without setting a response body to the error text, because this possibly may reveal some // sensitive internal infrastructure details func (r Response) WithError(err error) Response { resp := r.WithBody(err.Error()) @@ -157,25 +219,29 @@ func (r Response) Headers() []string { return r.headers } -// File returns response filename and error handler. Usually used by core only -func (r Response) File() (string, FileErrHandler) { - return r.filename, r.handler +// Attachment returns response's attachment. +// +// WARNING: do NEVER use this method in your code. It serves internal purposes ONLY +func (r Response) Attachment() Attachment { + return r.attachment } +// Clear discards everything was done with response object before func (r Response) Clear() Response { r.Code = status.OK r.Status = "" + r.ContentType = defaultContentType + r.TransferEncoding = "" r.headers = r.headers[:0] - r.filename = "" r.Body = nil - r.handler = nil - + r.attachment = Attachment{} return r } // bodyIOWriter is an implementation of io.Writer for response body type bodyIOWriter struct { response Response + readBuff []byte } func newBodyIOWriter(response Response) *bodyIOWriter { @@ -189,3 +255,25 @@ func (r *bodyIOWriter) Write(data []byte) (n int, err error) { return len(data), nil } + +func (r *bodyIOWriter) ReadFrom(reader io.Reader) (n int64, err error) { + const readBuffSize = 2048 + + if len(r.readBuff) == 0 { + r.readBuff = make([]byte, readBuffSize) + } + + for { + readN, readErr := reader.Read(r.readBuff) + _, _ = r.Write(r.readBuff[:n]) // bodyIOWriter.Write always returns n=len(data) and err=nil + n += int64(readN) + + switch readErr { + case nil: + case io.EOF: + return n, nil + default: + return n, readErr + } + } +} diff --git a/internal/render/engine.go b/internal/render/engine.go index 99c98861..b39f06d1 100644 --- a/internal/render/engine.go +++ b/internal/render/engine.go @@ -1,13 +1,11 @@ package render import ( - "errors" "github.com/indigo-web/indigo/http/status" "github.com/indigo-web/indigo/internal/functools" "github.com/indigo-web/indigo/internal/render/types" "io" "math" - "os" "strconv" "strings" @@ -20,8 +18,7 @@ import ( var ( contentLength = []byte("Content-Length: ") - errConnWrite = errors.New("error occurred while communicating with conn") - errFileNotFound = errors.New("desired file not found") + emptyChunkedPart = []byte("0\r\n\r\n") ) type Engine interface { @@ -42,6 +39,10 @@ type engine struct { } func NewEngine(buff, fileBuff []byte, defaultHeaders map[string][]string) Engine { + return newEngine(buff, fileBuff, defaultHeaders) +} + +func newEngine(buff, fileBuff []byte, defaultHeaders map[string][]string) *engine { parsedDefaultHeaders := parseDefaultHeaders(defaultHeaders) return &engine{ @@ -68,16 +69,8 @@ func (e *engine) Write( e.renderProtocol(protocol) - if path, errhandler := response.File(); len(path) > 0 { - switch err := e.renderFile(request, response, writer); err { - case errFileNotFound: - e.clear() - - return e.Write(protocol, request, errhandler(status.ErrNotFound), writer) - default: - // nil will also be returned here - return err - } + if response.Attachment().Content() != nil { + return e.sendAttachment(request, response, writer) } e.renderHeaders(response) @@ -93,7 +86,7 @@ func (e *engine) Write( err = writer(e.buff) if !isKeepAlive(protocol, request) && request.Upgrade == proto.Unknown { - err = errConnWrite + err = status.ErrCloseConnection } return err @@ -107,7 +100,7 @@ func (e *engine) renderHeaders(response http.Response) { } else { // in case we have a custom response status text or unknown code, fallback to an old way e.buff = append(strconv.AppendInt(e.buff, int64(response.Code), 10), httpchars.SP...) - e.buff = append(append(e.buff, status.Text(response.Code)...), httpchars.CRLF...) + e.buff = append(append(e.buff, status.Text(response.Code)...), httpchars.CR, httpchars.LF) } responseHeaders := response.Headers() @@ -124,37 +117,37 @@ func (e *engine) renderHeaders(response http.Response) { e.renderHeader(e.defaultHeaders[i], e.defaultHeaders[i+1]) } + + // Content-Type is compulsory. Transfer-Encoding is not + // TODO: maybe, we can make similar to renderContentLength() functions for + // these well-known headers? This may a bit improve performance + e.renderHeader("Content-Type", response.ContentType) + if len(response.TransferEncoding) > 0 { + e.renderHeader("Transfer-Encoding", response.TransferEncoding) + } } -// renderFileInto opens a file in os.O_RDONLY mode, reading its size and appends -// a Content-Length header equal to size of the file, after that headers are being -// sent. Then 64kb buffer is allocated for reading from file and writing to the -// connection. In case network error occurred, errConnWrite is returned. Otherwise, -// received error is returned -// -// Not very elegant solution, but uploading files is not the main purpose of web-server. -// For small and medium projects, this may be enough, for anything serious - most of all -// nginx will be used (the same is about https) -func (e *engine) renderFile( +// sendAttachment simply encapsulates +func (e *engine) sendAttachment( request *http.Request, response http.Response, writer http.ResponseWriter, ) error { - filename, _ := response.File() - file, err := os.OpenFile(filename, os.O_RDONLY, 0) - if err != nil { - return errFileNotFound - } + attachment := response.Attachment() - stat, err := file.Stat() - if err != nil { - return err + if size := attachment.Size(); size >= 0 { + e.renderHeaders(response) + e.renderContentLength(int64(size)) + } else { + e.renderHeaders(response.WithTransferEncoding("chunked")) } - e.renderHeaders(response) - e.renderContentLength(stat.Size()) + // now we have to send the body via plain text or chunked transfer encoding. + // I'm proposing to make an exception for chunked transfer encoding with a + // separate method that'll handle with it by its own. Maybe, even for plain-text + e.crlf() - if err = writer(e.buff); err != nil { - return errConnWrite + if err := writer(e.buff); err != nil { + return status.ErrCloseConnection } if request.Method == methods.HEAD { @@ -163,25 +156,68 @@ func (e *engine) renderFile( return nil } - if e.fileBuff == nil { + if len(e.fileBuff) == 0 { // write by blocks 64kb each. Not really efficient, but in close future // file distributors will be implemented, so files uploading capabilities // will be extended e.fileBuff = make([]byte, math.MaxUint16) } + if size := attachment.Size(); size >= 0 { + return e.writePlainBody(attachment.Content(), writer) + } + + return e.writeChunkedBody(attachment.Content(), writer) +} + +func (e *engine) writePlainBody(r io.Reader, writer http.ResponseWriter) error { for { - n, err := file.Read(e.fileBuff) + n, err := r.Read(e.fileBuff) switch err { case nil: case io.EOF: return nil default: - return errConnWrite + return status.ErrCloseConnection } if err = writer(e.fileBuff[:n]); err != nil { - return errConnWrite + return status.ErrCloseConnection + } + } +} + +func (e *engine) writeChunkedBody(r io.Reader, writer http.ResponseWriter) error { + const ( + hexValueOffset = 8 + crlfSize = 1 /* CR */ + 1 /* LF */ + buffOffset = hexValueOffset + crlfSize + ) + + for { + n, err := r.Read(e.fileBuff[buffOffset : len(e.fileBuff)-crlfSize]) + + if n > 0 { + // first rewrite begin of the fileBuff to contain our hexdecimal value + buff := strconv.AppendUint(e.fileBuff[:0], uint64(n), 16) + // now we can determine the length of the hexdecimal value and make an + // offset for it + blankSpace := hexValueOffset - len(buff) + copy(e.fileBuff[blankSpace:], buff) + copy(e.fileBuff[hexValueOffset:], httpchars.CRLF) + copy(e.fileBuff[buffOffset+n:], httpchars.CRLF) + + if err := writer(e.fileBuff[blankSpace : buffOffset+n+crlfSize]); err != nil { + return status.ErrCloseConnection + } + } + + switch err { + case nil: + case io.EOF: + return writer(emptyChunkedPart) + default: + return status.ErrCloseConnection } } } From 5e4f54bc1618023fc021f928a17e46703b8d5d6a Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Sun, 5 Mar 2023 23:53:13 +0100 Subject: [PATCH 03/19] removed Content-Type default header ...as no more need in it anymore since engine inserts it by its own --- indi.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/indi.go b/indi.go index 879ac58b..abe81e58 100644 --- a/indi.go +++ b/indi.go @@ -20,21 +20,12 @@ import ( "github.com/indigo-web/indigo/settings" ) -const ( - // actually, we don't know what content type of body user responds - // with, so due to rfc2068 7.2.1 it is supposed to be - // application/octet-stream, but we know that it is usually text/html, - // isn't it? - defaultContentType = "text/html" -) - // DefaultHeaders are headers that are going to be sent unless they were overridden by // user. // // WARNING: if you want to edit them, do it using Application.AddDefaultHeader or // Application.DeleteDefaultHeader instead var DefaultHeaders = map[string][]string{ - "Content-Type": {defaultContentType}, // nil here means that value will be set later, when server will be initializing "Accept-Encodings": nil, } From c7e9402872673bc5cb987c44e4ff12b9d0589905 Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Sun, 5 Mar 2023 23:53:29 +0100 Subject: [PATCH 04/19] updated tests and examples due to changes --- examples/combined/combined.go | 7 +++- indigo_test.go | 15 ++++--- internal/render/engine_test.go | 56 +++++++++++++++++++++++-- internal/server/http/http_bench_test.go | 35 +++++++++------- 4 files changed, 84 insertions(+), 29 deletions(-) diff --git a/examples/combined/combined.go b/examples/combined/combined.go index 35c1956b..d27798d1 100644 --- a/examples/combined/combined.go +++ b/examples/combined/combined.go @@ -19,13 +19,16 @@ var ( ) func Index(request *http.Request) http.Response { - return http.RespondTo(request).WithFile(index, func(err error) http.Response { + resp, err := http.RespondTo(request).WithFile(index) + if err != nil { return http.RespondTo(request). WithCode(status.NotFound). WithBody( index + ": not found; try running this example directly from examples/combined folder", ) - }) + } + + return resp } func IndexSay(request *http.Request) http.Response { diff --git a/indigo_test.go b/indigo_test.go index 9d542ee9..5a45ae33 100644 --- a/indigo_test.go +++ b/indigo_test.go @@ -113,17 +113,16 @@ func getStaticRouter(t *testing.T) router.Router { }) with.Get("file", func(request *http.Request) http.Response { - return http.RespondTo(request).WithFile(testFilename, func(err error) http.Response { - t.Fail() // this callback must never be called - - return http.RespondTo(request) - }) + resp, err := http.RespondTo(request).WithFile(testFilename) + require.NoError(t, err) + return resp }) with.Get("file-notfound", func(request *http.Request) http.Response { - return http.RespondTo(request).WithFile(testFilename+"notfound", func(err error) http.Response { - return http.RespondTo(request).WithBody(testFileIfNotFound) - }) + _, err := http.RespondTo(request).WithFile(testFilename + "non-existing") + require.Error(t, err) + + return http.RespondTo(request).WithBody(testFileIfNotFound) }) // request.OnBody() is not tested because request.Body() (wrapper for OnBody) diff --git a/internal/render/engine_test.go b/internal/render/engine_test.go index 7b9fa932..81261c9f 100644 --- a/internal/render/engine_test.go +++ b/internal/render/engine_test.go @@ -14,12 +14,14 @@ import ( "github.com/indigo-web/indigo/settings" "github.com/stretchr/testify/require" "io" + "math" stdhttp "net/http" + "strings" "testing" ) -func getEngine(defaultHeaders map[string][]string) Engine { - return NewEngine(make([]byte, 0, 1024), nil, defaultHeaders) +func getEngine(defaultHeaders map[string][]string) *engine { + return newEngine(make([]byte, 0, 1024), nil, defaultHeaders) } func newRequest() *http.Request { @@ -47,7 +49,9 @@ func TestEngine_Write(t *testing.T) { require.NoError(t, err) resp, err := stdhttp.ReadResponse(bufio.NewReader(bytes.NewBuffer(data)), r) require.Equal(t, 200, resp.StatusCode) - require.Equal(t, 1, len(resp.Header)) + require.Equal(t, 2, len(resp.Header)) + require.Contains(t, resp.Header, "Content-Length") + require.Contains(t, resp.Header, "Content-Type") body, err := io.ReadAll(resp.Body) require.NoError(t, err) require.Empty(t, body) @@ -131,7 +135,7 @@ func TestEngine_Write(t *testing.T) { return nil }) - require.EqualError(t, err, errConnWrite.Error()) + require.EqualError(t, err, status.ErrCloseConnection.Error()) }) t.Run("CustomCodeAndStatus", func(t *testing.T) { @@ -196,3 +200,47 @@ func TestEngine_PreWrite(t *testing.T) { require.Equal(t, "HTTP/1.1", resp.Proto) }) } + +func TestEngine_ChunkedTransfer(t *testing.T) { + t.Run("Simple", func(t *testing.T) { + reader := bytes.NewBuffer([]byte("Hello, world!")) + wantData := "d\r\nHello, world!\r\n0\r\n\r\n" + renderer := getEngine(nil) + renderer.fileBuff = make([]byte, math.MaxUint16) + + var data []byte + err := renderer.writeChunkedBody(reader, func(b []byte) error { + data = append(data, b...) + return nil + }) + require.NoError(t, err) + require.Equal(t, wantData, string(data)) + }) + + t.Run("Long", func(t *testing.T) { + const buffSize = 64 + parser := http1.NewChunkedBodyParser(settings.Default().Body) + payload := strings.Repeat("abcdefgh", 10*buffSize) + reader := bytes.NewBuffer([]byte(payload)) + renderer := getEngine(nil) + renderer.fileBuff = make([]byte, buffSize) + + var data []byte + err := renderer.writeChunkedBody(reader, func(b []byte) error { + for len(b) > 0 { + chunk, extra, err := parser.Parse(b, false) + if err == io.EOF { + return nil + } + + require.NoError(t, err) + data = append(data, chunk...) + b = extra + } + + return nil + }) + require.NoError(t, err) + require.Equal(t, payload, string(data)) + }) +} diff --git a/internal/server/http/http_bench_test.go b/internal/server/http/http_bench_test.go index 727fbdfc..0833ee58 100644 --- a/internal/server/http/http_bench_test.go +++ b/internal/server/http/http_bench_test.go @@ -72,21 +72,15 @@ func BenchmarkIndigo(b *testing.B) { return http.RespondTo(request).WithHeader("Hello", "World") }) - router.Get("/with-two-headers", func(request *http.Request) http.Response { - return http.RespondTo(request). - WithHeader("Hello", "World"). - WithHeader("Lorem", "Ipsum") - }) - router.OnStart() s := settings.Default() q := query.NewQuery(func() map[string][]byte { return make(map[string][]byte) }) - bodyReader := http1.NewBodyReader(dummy.NewNopClient(), settings.Default().Body) + bodyReader := http1.NewBodyReader(dummy.NewNopClient(), s.Body) request := http.NewRequest( - headers.NewHeaders(nil), q, http.NewResponse(), dummy.NewNopConn(), bodyReader, + headers.NewHeaders(make(map[string][]string, 10)), q, http.NewResponse(), dummy.NewNopConn(), bodyReader, ) keyAllocator := alloc.NewAllocator( s.Headers.MaxKeyLength*s.Headers.Number.Default, @@ -105,6 +99,8 @@ func BenchmarkIndigo(b *testing.B) { simpleGETClient := dummy.NewCircularClient(simpleGETRequest) b.Run("SimpleGET", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { server.RunOnce(simpleGETClient, request, bodyReader, render, parser) } @@ -112,6 +108,8 @@ func BenchmarkIndigo(b *testing.B) { fiveHeadersGETClient := dummy.NewCircularClient(fiveHeadersGETRequest) b.Run("FiveHeadersGET", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { server.RunOnce(fiveHeadersGETClient, request, bodyReader, render, parser) } @@ -119,6 +117,8 @@ func BenchmarkIndigo(b *testing.B) { tenHeadersGETClient := dummy.NewCircularClient(tenHeadersGETRequest) b.Run("TenHeadersGET", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { server.RunOnce(tenHeadersGETClient, request, bodyReader, render, parser) } @@ -126,16 +126,21 @@ func BenchmarkIndigo(b *testing.B) { withRespHeadersGETClient := dummy.NewCircularClient(simpleGETWithHeader) b.Run("WithRespHeader", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { server.RunOnce(withRespHeadersGETClient, request, bodyReader, render, parser) } }) - // TODO: add here some special request with special client that is able to - // parse a body - //b.Run("SimplePOST", func(b *testing.B) { - // for i := 0; i < b.N; i++ { - // _ = server.OnData(simplePOST) - // } - //}) + withBodyClient := dummy.NewCircularClient(simplePOST) + b.Run("SimplePOST", func(b *testing.B) { + reader := http1.NewBodyReader(withBodyClient, settings.Default().Body) + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + server.RunOnce(withBodyClient, request, reader, render, parser) + } + }) } From db33709cd4bd0c42ac617f1141bf9e883dd173c7 Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Sun, 5 Mar 2023 23:53:41 +0100 Subject: [PATCH 05/19] bumped version --- version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version b/version index 60a2d3e9..79a2734b 100644 --- a/version +++ b/version @@ -1 +1 @@ -0.4.0 \ No newline at end of file +0.5.0 \ No newline at end of file From 66300053f37e9ec58a76df224871421a654cb97f Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 19:46:25 +0100 Subject: [PATCH 06/19] added a constant for avoiding magic number in switch --- internal/parser/http1/body.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/parser/http1/body.go b/internal/parser/http1/body.go index 89e11ca8..8153c2c4 100644 --- a/internal/parser/http1/body.go +++ b/internal/parser/http1/body.go @@ -33,10 +33,12 @@ func (b *bodyReader) Init(request *http.Request) { } func (b *bodyReader) Read() ([]byte, error) { + const chunkedBody = -1 + switch b.bodyBytesLeft { case 0: return nil, io.EOF - case -1: + case chunkedBody: return b.chunkedBodyReader() default: return b.plainBodyReader() From a6bad21936bf93e902fc46eb43f07369aaac40d3 Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 20:23:34 +0100 Subject: [PATCH 07/19] added new setting `URL.Params.DisableMapClear` new setting allows user to disable a map clear operation for dynamic path parameters --- settings/settings.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/settings/settings.go b/settings/settings.go index 8ec41817..f2572e67 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -18,6 +18,12 @@ type ( } HeadersValuesObjectPoolSize = int MaxURLLength int + URLParams struct { + // This option allows user to disable the automatic path params map clearing. + // May be useful in cases where params keys are being accessed directly only, + // and nothing tries to get all the map values + DisableMapClear bool + } Query struct { // MaxLength is responsible for a limit of the query length @@ -74,6 +80,7 @@ type ( // until client disconnect MaxLength MaxURLLength Query Query + Params URLParams } TCP struct { @@ -134,6 +141,9 @@ func Default() Settings { MaxLength: math.MaxUint16, DefaultMapSize: 20, }, + Params: URLParams{ + DisableMapClear: false, + }, }, TCP: TCP{ ReadBufferSize: 2048, @@ -174,6 +184,7 @@ func Fill(original Settings) (modified Settings) { original.URL.Query.MaxLength, defaultSettings.URL.Query.MaxLength) original.URL.Query.DefaultMapSize = customOrDefault( original.URL.Query.DefaultMapSize, defaultSettings.URL.Query.DefaultMapSize) + /* skip original.URL.Params.DisableMapClear, as its zero value is already default one */ original.TCP.ReadBufferSize = customOrDefault( original.TCP.ReadBufferSize, defaultSettings.TCP.ReadBufferSize) original.TCP.ReadTimeout = customOrDefault( From ddea8b811849d67576c11fdcec964c8d0058d80f Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 20:26:01 +0100 Subject: [PATCH 08/19] updated `Request.Path` attribute now it is a struct with its string value and parameters (that is a map corresponding to dynamic path segments). Also added all the corresponding logic (clearing operation, that may be set up by the bool flag provided through the constructor) --- http/request.go | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/http/request.go b/http/request.go index dae09a21..4b97f3fe 100644 --- a/http/request.go +++ b/http/request.go @@ -23,7 +23,13 @@ type ( ) type ( - Path = string + Params = map[string]string + + Path struct { + String string + Params Params + } + Fragment = string ) @@ -47,10 +53,11 @@ type Request struct { body BodyReader bodyBuff []byte - Ctx context.Context - response Response - conn net.Conn - wasHijacked bool + Ctx context.Context + response Response + conn net.Conn + wasHijacked bool + clearParamsMap bool } // NewRequest returns a new instance of request object and body gateway @@ -58,22 +65,20 @@ type Request struct { // HTTP/1.1 as a protocol by default is set because if first request from user // is invalid, we need to render a response using request method, but appears // that default method is a null-value (proto.Unknown) -// Also query.Query is being constructed right here instead of passing from outside -// because it has only optional purposes and buff will be nil anyway -// But maybe it's better to implement DI all the way we go? I don't know, maybe -// someone will contribute and fix this func NewRequest( hdrs headers.Headers, query query.Query, response Response, conn net.Conn, body BodyReader, + disableParamsMapClearing bool, ) *Request { request := &Request{ - Query: query, - Proto: proto.HTTP11, - Headers: hdrs, - Remote: conn.RemoteAddr(), - conn: conn, - body: body, - Ctx: context.Background(), - response: response, + Query: query, + Proto: proto.HTTP11, + Headers: hdrs, + Remote: conn.RemoteAddr(), + conn: conn, + body: body, + Ctx: context.Background(), + response: response, + clearParamsMap: !disableParamsMapClearing, } return request @@ -173,6 +178,12 @@ func (r *Request) Clear() (err error) { r.IsChunked = false r.Upgrade = proto.Unknown + if r.clearParamsMap && len(r.Path.Params) > 0 { + for k := range r.Path.Params { + delete(r.Path.Params, k) + } + } + return nil } From a5c89b574387e34b2f04d119ffc5de8fc1953fd6 Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 20:27:06 +0100 Subject: [PATCH 09/19] updated types in path query parser now path query is a `map[string]string` instead of non-comfortable `map[string][]byte` --- http/query/query.go | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/http/query/query.go b/http/query/query.go index 2404c9bd..b5fe53a6 100644 --- a/http/query/query.go +++ b/http/query/query.go @@ -9,21 +9,20 @@ import ( var ErrNoSuchKey = errors.New("desired key does not exists") type ( - rawQuery []byte - parsedQuery map[string][]byte - - queryFactory func() map[string][]byte + rawQuery []byte + Map = map[string]string + mapFactory func() Map ) // Query is optional, it may contain rawQuery, but it will not be parsed until // needed type Query struct { rawQuery rawQuery - parsedQuery parsedQuery - queryFactory queryFactory + parsedQuery Map + queryFactory mapFactory } -func NewQuery(queryFactory queryFactory) Query { +func NewQuery(queryFactory mapFactory) Query { return Query{ queryFactory: queryFactory, } @@ -37,16 +36,16 @@ func (q *Query) Set(raw []byte) { q.parsedQuery = nil } -// Get is responsible for getting a key from query query. In case this +// Get is responsible for getting a key from query. In case this // method is called a first time since rawQuery was set (or not set // at all), rawQuery bytearray will be parsed and value returned // (or ErrNoSuchKey instead). In case of invalid query bytearray, // ErrBadQuery will be returned -func (q *Query) Get(key string) (value []byte, err error) { +func (q *Query) Get(key string) (value string, err error) { if q.parsedQuery == nil { q.parsedQuery, err = queryparser.Parse(q.rawQuery, q.queryFactory) if err != nil { - return nil, err + return "", err } } @@ -59,6 +58,6 @@ func (q *Query) Get(key string) (value []byte, err error) { } // Raw just returns a raw value of query as it is -func (q Query) Raw() []byte { +func (q *Query) Raw() []byte { return q.rawQuery } From 877baa390fc2ba57c32dc358491fb5782cb4709b Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 20:27:48 +0100 Subject: [PATCH 10/19] updated queries map type switched from []byte-values to string-values --- internal/queryparser/parser.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/queryparser/parser.go b/internal/queryparser/parser.go index dfa83ca0..67b5f490 100644 --- a/internal/queryparser/parser.go +++ b/internal/queryparser/parser.go @@ -5,7 +5,7 @@ import ( "github.com/indigo-web/indigo/internal" ) -func Parse(data []byte, queryMapFactory func() map[string][]byte) (queries map[string][]byte, err error) { +func Parse(data []byte, queryMapFactory func() map[string]string) (queries map[string]string, err error) { // TODO: make queryMapFactory map[string][]string, just like headers var ( @@ -34,7 +34,7 @@ func Parse(data []byte, queryMapFactory func() map[string][]byte) (queries map[s } case eValue: if data[i] == '&' { - queries[key] = data[offset:i] + queries[key] = internal.B2S(data[offset:i]) offset = i + 1 state = eKey } @@ -45,7 +45,7 @@ func Parse(data []byte, queryMapFactory func() map[string][]byte) (queries map[s return nil, status.ErrBadQuery } - queries[key] = data[offset:] + queries[key] = internal.B2S(data[offset:]) return queries, nil } From 8ed6733f5ac754e64e07fc5316065de07cc1ae60 Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 20:28:23 +0100 Subject: [PATCH 11/19] increased file buff size 64kb -> 128kb --- internal/render/engine.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/render/engine.go b/internal/render/engine.go index b39f06d1..ba3f1737 100644 --- a/internal/render/engine.go +++ b/internal/render/engine.go @@ -5,7 +5,6 @@ import ( "github.com/indigo-web/indigo/internal/functools" "github.com/indigo-web/indigo/internal/render/types" "io" - "math" "strconv" "strings" @@ -35,7 +34,7 @@ type engine struct { buff, fileBuff []byte buffOffset int defaultHeaders, defaultHeadersReserve types.DefaultHeaders - // TODO: add files distribution mechanism (and edit docstring) + // TODO: add files distribution mechanism (and probably edit docstring) } func NewEngine(buff, fileBuff []byte, defaultHeaders map[string][]string) Engine { @@ -160,7 +159,8 @@ func (e *engine) sendAttachment( // write by blocks 64kb each. Not really efficient, but in close future // file distributors will be implemented, so files uploading capabilities // will be extended - e.fileBuff = make([]byte, math.MaxUint16) + const fileBuffSize = 128 /* kilobytes */ * 1024 /* bytes */ + e.fileBuff = make([]byte, fileBuffSize) } if size := attachment.Size(); size >= 0 { @@ -171,6 +171,10 @@ func (e *engine) sendAttachment( } func (e *engine) writePlainBody(r io.Reader, writer http.ResponseWriter) error { + // TODO: implement checking whether r implements io.ReaderAt interface. In case it does + // body may be transferred more efficiently. This requires implementing io.Writer + // http.ResponseWriter + for { n, err := r.Read(e.fileBuff) switch err { From 62d33d35083cad011d3297111fc7a6e7798c2086 Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 20:29:49 +0100 Subject: [PATCH 12/19] removed useless client close call removed useless `client.Close()` from `RunOnce()` as this already makes `Run()` --- internal/server/http/http.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/server/http/http.go b/internal/server/http/http.go index a8d59e58..617e216d 100644 --- a/internal/server/http/http.go +++ b/internal/server/http/http.go @@ -83,7 +83,6 @@ func (h *httpServer) RunOnce( response := h.router.OnRequest(req) if req.WasHijacked() { - _ = client.Close() return false } From 238a29e4348a5da911899956c0f4c8e88c77eec1 Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 20:31:49 +0100 Subject: [PATCH 13/19] switched from context to map now radix tree uses hashmap for keeping dynamic segments values instead of context.Context. This is much more efficient and better from the UX point of view solution --- router/inbuilt/radix/tree.go | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/router/inbuilt/radix/tree.go b/router/inbuilt/radix/tree.go index 9074f56a..783127d6 100644 --- a/router/inbuilt/radix/tree.go +++ b/router/inbuilt/radix/tree.go @@ -1,17 +1,17 @@ package radix import ( - "context" "errors" "github.com/indigo-web/indigo/router/inbuilt/types" - "github.com/indigo-web/indigo/valuectx" ) var ErrNotImplemented = errors.New( "different dynamic segment names are not allowed for common path prefix", ) +type Params map[string]string + type Payload struct { MethodsMap types.MethodsMap Allow string @@ -20,7 +20,7 @@ type Payload struct { type Tree interface { Insert(Template, Payload) error MustInsert(Template, Payload) - Match(context.Context, string) (context.Context, *Payload) + Match(Params, string) *Payload } type Node struct { @@ -90,10 +90,10 @@ func (n *Node) insertRecursively(segments []Segment, payload *Payload) error { return node.insertRecursively(segments[1:], payload) } -func (n *Node) Match(ctx context.Context, path string) (context.Context, *Payload) { +func (n *Node) Match(params Params, path string) *Payload { if path[0] != '/' { // all http request paths MUST have a leading slash - return ctx, n.payload + return nil } path = path[1:] @@ -106,9 +106,9 @@ func (n *Node) Match(ctx context.Context, path string) (context.Context, *Payloa for i := range path { if path[i] == '/' { var ok bool - ctx, node, ok = processSegment(ctx, path[offset:i], node) + node, ok = processSegment(params, path[offset:i], node) if !ok { - return ctx, nil + return nil } offset = i + 1 @@ -117,27 +117,27 @@ func (n *Node) Match(ctx context.Context, path string) (context.Context, *Payloa if offset < len(path) { var ok bool - ctx, node, ok = processSegment(ctx, path[offset:], node) + node, ok = processSegment(params, path[offset:], node) if !ok { - return ctx, nil + return nil } } - return ctx, node.payload + return node.payload } -func processSegment(ctx context.Context, segment string, node *Node) (context.Context, *Node, bool) { +func processSegment(params Params, segment string, node *Node) (*Node, bool) { if nextNode, found := node.staticSegments[segment]; found { - return ctx, nextNode, true + return nextNode, true } if !node.isDynamic || len(segment) == 0 { - return ctx, nil, false + return nil, false } if len(node.dynamicName) > 0 { - ctx = valuectx.WithValue(ctx, node.dynamicName, segment) + params[node.dynamicName] = segment } - return ctx, node.next, true + return node.next, true } From 15d408e9cffc68ad563a8e0e57c0abd282c2097e Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 20:33:35 +0100 Subject: [PATCH 14/19] minor code improvements moved the check of a first char from loop to the function's body. Not to say that this matters from the performance's point of view, but more about visual simplifying the code --- router/inbuilt/radix/template.go | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/router/inbuilt/radix/template.go b/router/inbuilt/radix/template.go index 0549ede6..6ca98d51 100644 --- a/router/inbuilt/radix/template.go +++ b/router/inbuilt/radix/template.go @@ -38,19 +38,14 @@ func Parse(tmpl string) (Template, error) { return template, ErrEmptyPath } - for i, char := range tmpl { - if i == 0 { - if char != '/' { - return template, fmt.Errorf(`"%s": a leading slash is required`, tmpl) - } - - // skip leading slash - continue - } + if tmpl[0] != '/' { + return template, fmt.Errorf(`"%s": a leading slash is required`, tmpl) + } + for i := 1; i < len(tmpl); i++ { switch state { case eStatic: - switch char { + switch tmpl[i] { case '/': template.segments = append(template.segments, Segment{ IsDynamic: false, @@ -65,7 +60,7 @@ func Parse(tmpl string) (Template, error) { ) } case eSlash: - switch char { + switch tmpl[i] { case '/': case '{': offset = i + 1 @@ -74,7 +69,7 @@ func Parse(tmpl string) (Template, error) { state = eStatic } case eDynamic: - switch char { + switch tmpl[i] { case '}': template.segments = append(template.segments, Segment{ IsDynamic: true, @@ -88,7 +83,7 @@ func Parse(tmpl string) (Template, error) { ) } case eFinishDynamic: - switch char { + switch tmpl[i] { case '/': offset = i + 1 state = eSlash From fee17b5ca7b8e4c77632dea899ec7f5fc3d63d03 Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 20:34:10 +0100 Subject: [PATCH 15/19] minor code improvements naming updates, capability with the new API --- router/inbuilt/obtainer/dynamic.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/router/inbuilt/obtainer/dynamic.go b/router/inbuilt/obtainer/dynamic.go index 4809d0d8..ac0077c8 100644 --- a/router/inbuilt/obtainer/dynamic.go +++ b/router/inbuilt/obtainer/dynamic.go @@ -10,16 +10,15 @@ import ( "github.com/indigo-web/indigo/internal/functools" "github.com/indigo-web/indigo/internal/mapconv" "github.com/indigo-web/indigo/router/inbuilt/radix" - routertypes "github.com/indigo-web/indigo/router/inbuilt/types" + "github.com/indigo-web/indigo/router/inbuilt/types" "github.com/indigo-web/indigo/valuectx" ) -func DynamicObtainer(routes routertypes.RoutesMap) Obtainer { +func DynamicObtainer(routes types.RoutesMap) Obtainer { tree := getTree(routes) - return func(req *http.Request) (routertypes.HandlerFunc, error) { - var payload *radix.Payload - req.Ctx, payload = tree.Match(req.Ctx, stripTrailingSlash(req.Path)) + return func(req *http.Request) (types.HandlerFunc, error) { + payload := tree.Match(req.Path.Params, stripTrailingSlash(req.Path.String)) if payload == nil { return nil, status.ErrNotFound } @@ -35,7 +34,7 @@ func DynamicObtainer(routes routertypes.RoutesMap) Obtainer { } } -func getTree(routes routertypes.RoutesMap) radix.Tree { +func getTree(routes types.RoutesMap) radix.Tree { tree := radix.NewTree() for k, v := range routes { From 242b974da08add638c297be63641d29c3d31ffb5 Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 20:37:35 +0100 Subject: [PATCH 16/19] capability with the new API --- http/query/query_test.go | 6 +-- indi.go | 8 ++-- internal/parser/http1/body_test.go | 2 +- internal/parser/http1/requestsparser_test.go | 3 +- internal/queryparser/parser_test.go | 4 +- internal/queryparser/parserbench_test.go | 13 +++---- internal/render/engine_test.go | 1 + internal/render/enginebench_test.go | 2 +- .../inbuilt/inbuiltrouter_middlewares_test.go | 11 +++--- router/inbuilt/inbuiltrouter_test.go | 2 +- router/inbuilt/obtainer/obtainer_test.go | 4 +- router/inbuilt/obtainer/static.go | 4 +- router/inbuilt/radix/bench_test.go | 11 +++--- router/inbuilt/radix/match_test.go | 37 +++++++++---------- router/inbuilt/route_bench_test.go | 18 +++------ 15 files changed, 62 insertions(+), 64 deletions(-) diff --git a/http/query/query_test.go b/http/query/query_test.go index 9a9c58cc..4ec6116c 100644 --- a/http/query/query_test.go +++ b/http/query/query_test.go @@ -10,8 +10,8 @@ func TestQuery(t *testing.T) { // here we test laziness of query // just test that passed buffer's content will not be used - query := NewQuery(func() map[string][]byte { - return make(map[string][]byte) + query := NewQuery(func() Map { + return make(Map) }) query.Set([]byte("hello=world")) require.Equal(t, "hello=world", string(query.rawQuery)) @@ -20,7 +20,7 @@ func TestQuery(t *testing.T) { t.Run("GetExistingKey", func(t *testing.T) { value, err := query.Get("hello") require.NoError(t, err) - require.Equal(t, "world", string(value)) + require.Equal(t, "world", value) }) t.Run("GetNonExistingKey", func(t *testing.T) { diff --git a/indi.go b/indi.go index abe81e58..f6c732c4 100644 --- a/indi.go +++ b/indi.go @@ -105,13 +105,14 @@ func (a *Application) Serve(r router.Router, optionalSettings ...settings.Settin s.Headers.ValueSpace.Maximal, ) objPool := pool.NewObjectPool[[]string](s.Headers.MaxValuesObjectPoolSize) - q := query.NewQuery(func() map[string][]byte { - return make(map[string][]byte, s.URL.Query.DefaultMapSize) + q := query.NewQuery(func() query.Map { + return make(query.Map, s.URL.Query.DefaultMapSize) }) hdrs := headers.NewHeaders(make(map[string][]string, s.Headers.Number.Default)) response := http.NewResponse() bodyReader := http1.NewBodyReader(client, s.Body) - request := http.NewRequest(hdrs, q, response, conn, bodyReader) + request := http.NewRequest(hdrs, q, response, conn, bodyReader, s.URL.Params.DisableMapClear) + request.Path.Params = make(http.Params) startLineBuff := make([]byte, s.URL.MaxLength) httpParser := http1.NewHTTPRequestsParser( @@ -141,6 +142,7 @@ func (a *Application) Wait() { <-a.shutdown } +// getSettings converts optional settings to concrete func getSettings(s ...settings.Settings) (settings.Settings, error) { switch len(s) { case 0: diff --git a/internal/parser/http1/body_test.go b/internal/parser/http1/body_test.go index e24b9375..09ed8fb3 100644 --- a/internal/parser/http1/body_test.go +++ b/internal/parser/http1/body_test.go @@ -40,7 +40,7 @@ func getRequestWithReader(chunked bool, body ...[]byte) (*http.Request, http.Bod } request := http.NewRequest( - hdrs, query.Query{}, http.NewResponse(), dummy.NewNopConn(), reader, + hdrs, query.Query{}, http.NewResponse(), dummy.NewNopConn(), reader, false, ) request.ContentLength = contentLength request.IsChunked = chunked diff --git a/internal/parser/http1/requestsparser_test.go b/internal/parser/http1/requestsparser_test.go index 6c78ee10..ca0318a2 100644 --- a/internal/parser/http1/requestsparser_test.go +++ b/internal/parser/http1/requestsparser_test.go @@ -51,6 +51,7 @@ func getParser() (httpparser.HTTPRequestsParser, *http.Request) { body := NewBodyReader(dummy.NewNopClient(), s.Body) request := http.NewRequest( headers.NewHeaders(nil), query.Query{}, http.NewResponse(), dummy.NewNopConn(), body, + false, ) startLineBuff := make([]byte, s.URL.MaxLength) @@ -68,7 +69,7 @@ type wantedRequest struct { func compareRequests(t *testing.T, wanted wantedRequest, actual *http.Request) { require.Equal(t, wanted.Method, actual.Method) - require.Equal(t, wanted.Path, actual.Path) + require.Equal(t, wanted.Path, actual.Path.String) require.Equal(t, wanted.Protocol, actual.Proto) for key, values := range wanted.Headers.Unwrap() { diff --git a/internal/queryparser/parser_test.go b/internal/queryparser/parser_test.go index e8cc23b9..ca40f49a 100644 --- a/internal/queryparser/parser_test.go +++ b/internal/queryparser/parser_test.go @@ -7,8 +7,8 @@ import ( "github.com/stretchr/testify/require" ) -func defaultFactory() map[string][]byte { - return make(map[string][]byte) +func defaultFactory() map[string]string { + return make(map[string]string) } func TestParse_Positive(t *testing.T) { diff --git a/internal/queryparser/parserbench_test.go b/internal/queryparser/parserbench_test.go index 4d8612dd..e1fef6c9 100644 --- a/internal/queryparser/parserbench_test.go +++ b/internal/queryparser/parserbench_test.go @@ -2,15 +2,14 @@ package queryparser import "testing" -const initialQueryMapSize = 10 - -var queriesMap = make(map[string][]byte, initialQueryMapSize) - func BenchmarkParse(b *testing.B) { - queriesFactory := func() map[string][]byte { - return make(map[string][]byte, initialQueryMapSize) + const initialQueryMapSize = 10 + queriesMap := make(map[string]string, initialQueryMapSize) + + queriesFactory := func() map[string]string { + return make(map[string]string, initialQueryMapSize) } - queriesFactoryNoAlloc := func() map[string][]byte { + queriesFactoryNoAlloc := func() map[string]string { return queriesMap } diff --git a/internal/render/engine_test.go b/internal/render/engine_test.go index 81261c9f..86ca1c95 100644 --- a/internal/render/engine_test.go +++ b/internal/render/engine_test.go @@ -31,6 +31,7 @@ func newRequest() *http.Request { dummy.NewNopClient(), settings.Default().Body, ), + false, ) } diff --git a/internal/render/enginebench_test.go b/internal/render/enginebench_test.go index f5657a4e..1b718528 100644 --- a/internal/render/enginebench_test.go +++ b/internal/render/enginebench_test.go @@ -47,7 +47,7 @@ func BenchmarkRenderer_Response(b *testing.B) { response := http.NewResponse() bodyReader := http1.NewBodyReader(dummy.NewNopClient(), settings.Default().Body) request := http.NewRequest( - hdrs, query.NewQuery(nil), http.NewResponse(), dummy.NewNopConn(), bodyReader, + hdrs, query.NewQuery(nil), http.NewResponse(), dummy.NewNopConn(), bodyReader, false, ) b.Run("DefaultResponse_NoDefHeaders", func(b *testing.B) { diff --git a/router/inbuilt/inbuiltrouter_middlewares_test.go b/router/inbuilt/inbuiltrouter_middlewares_test.go index eaff962d..0458b3bb 100644 --- a/router/inbuilt/inbuiltrouter_middlewares_test.go +++ b/router/inbuilt/inbuiltrouter_middlewares_test.go @@ -110,11 +110,12 @@ func getPointApplied2Middleware(stack *callstack) routertypes.Middleware { } func getRequest() *http.Request { - query := query.NewQuery(nil) + q := query.NewQuery(nil) bodyReader := http1.NewBodyReader(dummy.NewNopClient(), settings.Default().Body) return http.NewRequest( - headers.NewHeaders(nil), query, http.NewResponse(), dummy.NewNopConn(), bodyReader, + headers.NewHeaders(nil), q, http.NewResponse(), dummy.NewNopConn(), bodyReader, + false, ) } @@ -148,7 +149,7 @@ func TestMiddlewares(t *testing.T) { t.Run("/", func(t *testing.T) { request := getRequest() request.Method = methods.GET - request.Path = "/" + request.Path.String = "/" response := r.OnRequest(request) require.Equal(t, int(status.OK), int(response.Code)) @@ -164,7 +165,7 @@ func TestMiddlewares(t *testing.T) { t.Run("/api/v1/hello", func(t *testing.T) { request := getRequest() request.Method = methods.GET - request.Path = "/api/v1/hello" + request.Path.String = "/api/v1/hello" response := r.OnRequest(request) require.Equal(t, status.OK, response.Code) @@ -180,7 +181,7 @@ func TestMiddlewares(t *testing.T) { t.Run("/api/v2/world", func(t *testing.T) { request := getRequest() request.Method = methods.GET - request.Path = "/api/v2/world" + request.Path.String = "/api/v2/world" response := r.OnRequest(request) require.Equal(t, status.OK, response.Code) diff --git a/router/inbuilt/inbuiltrouter_test.go b/router/inbuilt/inbuiltrouter_test.go index 44825212..0def0165 100644 --- a/router/inbuilt/inbuiltrouter_test.go +++ b/router/inbuilt/inbuiltrouter_test.go @@ -56,7 +56,7 @@ func TestRoute(t *testing.T) { t.Run("HEAD", func(t *testing.T) { request := getRequest() request.Method = methods.HEAD - request.Path = "/" + request.Path.String = "/" resp := r.processRequest(request) // we have not registered any HEAD-method handler yet, so GET method diff --git a/router/inbuilt/obtainer/obtainer_test.go b/router/inbuilt/obtainer/obtainer_test.go index b222288e..73877f4b 100644 --- a/router/inbuilt/obtainer/obtainer_test.go +++ b/router/inbuilt/obtainer/obtainer_test.go @@ -26,9 +26,9 @@ func newRequest(path string, method methods.Method) *http.Request { hdrs := headers.NewHeaders(make(map[string][]string)) bodyReader := http1.NewBodyReader(dummy.NewNopClient(), settings.Default().Body) request := http.NewRequest( - hdrs, query.Query{}, http.NewResponse(), dummy.NewNopConn(), bodyReader, + hdrs, query.Query{}, http.NewResponse(), dummy.NewNopConn(), bodyReader, false, ) - request.Path = path + request.Path.String = path request.Method = method return request diff --git a/router/inbuilt/obtainer/static.go b/router/inbuilt/obtainer/static.go index 8cdf429f..a5545cc2 100644 --- a/router/inbuilt/obtainer/static.go +++ b/router/inbuilt/obtainer/static.go @@ -16,7 +16,7 @@ func StaticObtainer(routes types.RoutesMap) Obtainer { allowedMethods := getAllowedMethodsMap(routes) return func(req *http.Request) (types.HandlerFunc, error) { - methodsMap, found := routes[stripTrailingSlash(req.Path)] + methodsMap, found := routes[stripTrailingSlash(req.Path.String)] if !found { return nil, status.ErrNotFound } @@ -25,7 +25,7 @@ func StaticObtainer(routes types.RoutesMap) Obtainer { return handler, nil } - req.Ctx = valuectx.WithValue(req.Ctx, "allow", allowedMethods[req.Path]) + req.Ctx = valuectx.WithValue(req.Ctx, "allow", allowedMethods[req.Path.String]) return nil, status.ErrMethodNotAllowed } diff --git a/router/inbuilt/radix/bench_test.go b/router/inbuilt/radix/bench_test.go index 6dbc82ba..9e902bb0 100644 --- a/router/inbuilt/radix/bench_test.go +++ b/router/inbuilt/radix/bench_test.go @@ -1,7 +1,6 @@ package radix import ( - "context" "testing" routertypes "github.com/indigo-web/indigo/router/inbuilt/types" @@ -34,28 +33,30 @@ func BenchmarkTreeMatch(b *testing.B) { tree.MustInsert(MustParse(shortTemplateSample), payload) tree.MustInsert(MustParse(mediumTemplateSample), payload) tree.MustInsert(MustParse(longTemplateSample), payload) + const paramsMapDefaultSize = 5 + params := make(Params, paramsMapDefaultSize) b.Run("OnlyStatic", func(b *testing.B) { for i := 0; i < b.N; i++ { - tree.Match(context.Background(), staticSample) + tree.Match(params, staticSample) } }) b.Run("Short", func(b *testing.B) { for i := 0; i < b.N; i++ { - tree.Match(context.Background(), shortSample) + tree.Match(params, shortSample) } }) b.Run("Medium", func(b *testing.B) { for i := 0; i < b.N; i++ { - tree.Match(context.Background(), mediumSample) + tree.Match(params, mediumSample) } }) b.Run("Long", func(b *testing.B) { for i := 0; i < b.N; i++ { - tree.Match(context.Background(), longSample) + tree.Match(params, longSample) } }) } diff --git a/router/inbuilt/radix/match_test.go b/router/inbuilt/radix/match_test.go index 674522e3..9dcc2f94 100644 --- a/router/inbuilt/radix/match_test.go +++ b/router/inbuilt/radix/match_test.go @@ -1,19 +1,18 @@ package radix import ( - "context" "testing" - routertypes "github.com/indigo-web/indigo/router/inbuilt/types" + "github.com/indigo-web/indigo/router/inbuilt/types" "github.com/stretchr/testify/require" ) func TestNode_Match_Positive(t *testing.T) { tree := NewTree() + params := make(Params) payload := Payload{ - MethodsMap: routertypes.MethodsMap{}, - Allow: "", + MethodsMap: types.MethodsMap{}, } tree.MustInsert(MustParse(staticSample), payload) tree.MustInsert(MustParse(unnamedTemplateSample), payload) @@ -23,42 +22,42 @@ func TestNode_Match_Positive(t *testing.T) { tree.MustInsert(MustParse("/"), payload) t.Run("StaticMatch", func(t *testing.T) { - _, handler := tree.Match(context.Background(), staticSample) + handler := tree.Match(params, staticSample) require.NotNil(t, handler) }) t.Run("UnnamedMatch", func(t *testing.T) { - ctx, handler := tree.Match(context.Background(), unnamedSample) - require.Nil(t, ctx.Value("")) + handler := tree.Match(params, unnamedSample) + require.Empty(t, params[""]) require.NotNil(t, handler) }) t.Run("ShortTemplateMatch", func(t *testing.T) { - ctx, handler := tree.Match(context.Background(), shortSample) + handler := tree.Match(params, shortSample) require.NotNil(t, handler) - require.Equal(t, "some-very-long-world", ctx.Value("world")) + require.Equal(t, "some-very-long-world", params["world"]) }) t.Run("MediumTemplateMatch", func(t *testing.T) { - ctx, handler := tree.Match(context.Background(), mediumSample) + handler := tree.Match(params, mediumSample) require.NotNil(t, handler) - require.Equal(t, "world-finally-became", ctx.Value("world")) - require.Equal(t, "good-as-fuck", ctx.Value("good")) - require.Equal(t, "ok-let-it-be", ctx.Value("ok")) + require.Equal(t, "world-finally-became", params["world"]) + require.Equal(t, "good-as-fuck", params["good"]) + require.Equal(t, "ok-let-it-be", params["ok"]) }) t.Run("Root", func(t *testing.T) { - _, handler := tree.Match(context.Background(), "/") + handler := tree.Match(params, "/") require.NotNil(t, handler) }) } func TestNode_Match_Negative(t *testing.T) { tree := NewTree() + params := make(Params) payload := Payload{ - MethodsMap: routertypes.MethodsMap{}, - Allow: "", + MethodsMap: types.MethodsMap{}, } tree.MustInsert(MustParse(staticSample), payload) tree.MustInsert(MustParse(shortTemplateSample), payload) @@ -66,17 +65,17 @@ func TestNode_Match_Negative(t *testing.T) { tree.MustInsert(MustParse(longTemplateSample), payload) t.Run("EmptyDynamicPath_WithTrailingSlash", func(t *testing.T) { - _, handler := tree.Match(context.Background(), "/hello/") + handler := tree.Match(params, "/hello/") require.Nil(t, handler) }) t.Run("EmptyDynamicPath_NoTrailingSlash", func(t *testing.T) { - _, handler := tree.Match(context.Background(), "/hello") + handler := tree.Match(params, "/hello") require.Nil(t, handler) }) t.Run("EmptyDynamicPath_BetweenStatic", func(t *testing.T) { - _, handler := tree.Match(context.Background(), "/hello//very/good/ok") + handler := tree.Match(params, "/hello//very/good/ok") require.Nil(t, handler) }) } diff --git a/router/inbuilt/route_bench_test.go b/router/inbuilt/route_bench_test.go index 18f0dd30..46182bbc 100644 --- a/router/inbuilt/route_bench_test.go +++ b/router/inbuilt/route_bench_test.go @@ -4,35 +4,29 @@ import ( "strings" "testing" - "github.com/indigo-web/indigo/http" - methods "github.com/indigo-web/indigo/http/method" ) -func nopRender(_ http.Response) error { - return nil -} - func BenchmarkRequestRouting(b *testing.B) { longURIRequest := getRequest() longURIRequest.Method = methods.GET - longURIRequest.Path = "/" + strings.Repeat("a", 255) + longURIRequest.Path.String = "/" + strings.Repeat("a", 255) shortURIRequest := getRequest() shortURIRequest.Method = methods.GET - shortURIRequest.Path = "/" + strings.Repeat("a", 15) + shortURIRequest.Path.String = "/" + strings.Repeat("a", 15) unknownURIRequest := getRequest() unknownURIRequest.Method = methods.GET - unknownURIRequest.Path = "/" + strings.Repeat("b", 255) + unknownURIRequest.Path.String = "/" + strings.Repeat("b", 255) unknownMethodRequest := getRequest() unknownMethodRequest.Method = methods.POST - unknownMethodRequest.Path = longURIRequest.Path + unknownMethodRequest.Path.String = longURIRequest.Path.String router := NewRouter() - router.Get(longURIRequest.Path, nopHandler) - router.Get(shortURIRequest.Path, nopHandler) + router.Get(longURIRequest.Path.String, nopHandler) + router.Get(shortURIRequest.Path.String, nopHandler) router.OnStart() From 1072e8925edac71c26e3b162923c4b289ab8e47b Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 20:38:06 +0100 Subject: [PATCH 17/19] capability with the new API mistakenly did not add them into the previous commit --- internal/parser/http1/requestsparser.go | 14 +++++++------- router/inbuilt/trace.go | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/parser/http1/requestsparser.go b/internal/parser/http1/requestsparser.go index cd06b5c7..2de4f281 100644 --- a/internal/parser/http1/requestsparser.go +++ b/internal/parser/http1/requestsparser.go @@ -180,7 +180,7 @@ path: return parser.Error, nil, status.ErrBadRequest } - p.request.Path = internal.B2S(p.startLineBuff[p.begin:p.pointer]) + p.request.Path.String = internal.B2S(p.startLineBuff[p.begin:p.pointer]) data = data[i+1:] p.state = eProto goto proto @@ -189,9 +189,9 @@ path: p.state = ePathDecode1Char goto pathDecode1Char case '?': - p.request.Path = internal.B2S(p.startLineBuff[p.begin:p.pointer]) - if len(p.request.Path) == 0 { - p.request.Path = "/" + p.request.Path.String = internal.B2S(p.startLineBuff[p.begin:p.pointer]) + if len(p.request.Path.String) == 0 { + p.request.Path.String = "/" } p.begin = p.pointer @@ -199,9 +199,9 @@ path: p.state = eQuery goto query case '#': - p.request.Path = internal.B2S(p.startLineBuff[p.begin:p.pointer]) - if len(p.request.Path) == 0 { - p.request.Path = "/" + p.request.Path.String = internal.B2S(p.startLineBuff[p.begin:p.pointer]) + if len(p.request.Path.String) == 0 { + p.request.Path.String = "/" } p.begin = p.pointer diff --git a/router/inbuilt/trace.go b/router/inbuilt/trace.go index d57e7186..8402c0c7 100644 --- a/router/inbuilt/trace.go +++ b/router/inbuilt/trace.go @@ -33,7 +33,7 @@ func renderHTTPRequest(request *http.Request, buff []byte) []byte { } func requestURI(request *http.Request, buff []byte) []byte { - buff = append(buff, request.Path...) + buff = append(buff, request.Path.String...) if query := request.Query.Raw(); len(query) > 0 { buff = append(buff, '?') From aef6a1a34c14fd38d4d32aba333e3c03b6df0265 Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 20:39:22 +0100 Subject: [PATCH 18/19] added the choice of the router but the choice is hardcoded, as it is a pain in the ass if you want to parse flags in benchmarks. But in case, there are 2 functions with the same signature, so swapping them is a task of 2 seconds --- internal/server/http/http_bench_test.go | 64 ++++++++++++++++++++----- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/internal/server/http/http_bench_test.go b/internal/server/http/http_bench_test.go index 0833ee58..b644209d 100644 --- a/internal/server/http/http_bench_test.go +++ b/internal/server/http/http_bench_test.go @@ -1,7 +1,12 @@ package http import ( + method "github.com/indigo-web/indigo/http/method" + "github.com/indigo-web/indigo/http/status" "github.com/indigo-web/indigo/internal/server/tcp/dummy" + "github.com/indigo-web/indigo/router" + "github.com/indigo-web/indigo/router/inbuilt" + "github.com/indigo-web/indigo/router/simple" "testing" "github.com/indigo-web/indigo/http" @@ -12,7 +17,6 @@ import ( "github.com/indigo-web/indigo/internal/alloc" "github.com/indigo-web/indigo/internal/parser/http1" render2 "github.com/indigo-web/indigo/internal/render" - "github.com/indigo-web/indigo/router/inbuilt" "github.com/indigo-web/indigo/settings" ) @@ -54,9 +58,9 @@ var defaultHeaders = map[string][]string{ "Accept-Encodings": nil, } -func BenchmarkIndigo(b *testing.B) { - router := inbuilt.NewRouter() - root := router.Resource("/") +func getInbuiltRouter() router.Router { + r := inbuilt.NewRouter() + root := r.Resource("/") root.Get(http.RespondTo) root.Post(func(request *http.Request) http.Response { _ = request.OnBody(func([]byte) error { @@ -64,23 +68,59 @@ func BenchmarkIndigo(b *testing.B) { }, func() error { return nil }) - return http.RespondTo(request) }) - - router.Get("/with-header", func(request *http.Request) http.Response { + r.Get("/with-header", func(request *http.Request) http.Response { return http.RespondTo(request).WithHeader("Hello", "World") }) + r.OnStart() + + return r +} + +func getSimpleRouter() router.Router { + r := simple.NewRouter(func(request *http.Request) http.Response { + switch request.Path.String { + case "/": + switch request.Method { + case method.GET: + return http.RespondTo(request) + case method.POST: + _ = request.OnBody(func([]byte) error { + return nil + }, func() error { + return nil + }) + + return http.RespondTo(request) + default: + return http.RespondTo(request).WithError(status.ErrMethodNotAllowed) + } + case "/with-header": + return http.RespondTo(request).WithHeader("Hello", "World") + default: + return http.RespondTo(request). + WithError(status.ErrNotFound) + } + }, func(request *http.Request, err error) http.Response { + return http.RespondTo(request).WithError(err) + }) - router.OnStart() + return r +} +func BenchmarkIndigo(b *testing.B) { + // for benchmarking, using more realistic conditions. In case we want a pure performance - use + // getSimpleRouter() here. It is visibly faster + r := getInbuiltRouter() s := settings.Default() - q := query.NewQuery(func() map[string][]byte { - return make(map[string][]byte) + q := query.NewQuery(func() query.Map { + return make(query.Map) }) bodyReader := http1.NewBodyReader(dummy.NewNopClient(), s.Body) + hdrs := headers.NewHeaders(make(map[string][]string, 10)) request := http.NewRequest( - headers.NewHeaders(make(map[string][]string, 10)), q, http.NewResponse(), dummy.NewNopConn(), bodyReader, + hdrs, q, http.NewResponse(), dummy.NewNopConn(), bodyReader, false, ) keyAllocator := alloc.NewAllocator( s.Headers.MaxKeyLength*s.Headers.Number.Default, @@ -95,7 +135,7 @@ func BenchmarkIndigo(b *testing.B) { request, keyAllocator, valAllocator, objPool, startLineBuff, s.Headers, ) render := render2.NewEngine(make([]byte, 0, 1024), nil, defaultHeaders) - server := NewHTTPServer(router).(*httpServer) + server := NewHTTPServer(r).(*httpServer) simpleGETClient := dummy.NewCircularClient(simpleGETRequest) b.Run("SimpleGET", func(b *testing.B) { From e3d177f140ac511d670221fb59d6b176b6d5abf3 Mon Sep 17 00:00:00 2001 From: fakefloordiv Date: Thu, 9 Mar 2023 22:30:27 +0100 Subject: [PATCH 19/19] moved Query and Fragment into Path as Path is now a composite structure, moved other related attributes into it --- http/request.go | 27 ++++++++++--------- indi.go | 4 +-- indigo_test.go | 6 ++--- internal/parser/http1/body_test.go | 2 +- internal/parser/http1/requestsparser.go | 4 +-- internal/parser/http1/requestsparser_test.go | 4 +-- internal/render/engine_test.go | 2 +- internal/render/enginebench_test.go | 3 ++- internal/server/http/http_bench_test.go | 2 +- .../inbuilt/inbuiltrouter_middlewares_test.go | 2 +- router/inbuilt/obtainer/obtainer_test.go | 2 +- router/inbuilt/trace.go | 6 ++--- 12 files changed, 34 insertions(+), 30 deletions(-) diff --git a/http/request.go b/http/request.go index 4b97f3fe..dc21e572 100644 --- a/http/request.go +++ b/http/request.go @@ -26,8 +26,10 @@ type ( Params = map[string]string Path struct { - String string - Params Params + String string + Params Params + Query query.Query + Fragment Fragment } Fragment = string @@ -37,12 +39,10 @@ type ( // About headers manager see at http/headers/headers.go:Manager // Headers attribute references at that one that lays in manager type Request struct { - Method method.Method - Path Path - Query query.Query - Fragment Fragment - Proto proto.Proto - Remote net.Addr + Method method.Method + Path Path + Proto proto.Proto + Remote net.Addr Headers headers.Headers @@ -67,10 +67,13 @@ type Request struct { // that default method is a null-value (proto.Unknown) func NewRequest( hdrs headers.Headers, query query.Query, response Response, conn net.Conn, body BodyReader, - disableParamsMapClearing bool, + paramsMap Params, disableParamsMapClearing bool, ) *Request { request := &Request{ - Query: query, + Path: Path{ + Params: paramsMap, + Query: query, + }, Proto: proto.HTTP11, Headers: hdrs, Remote: conn.RemoteAddr(), @@ -165,8 +168,8 @@ func (r *Request) WasHijacked() bool { // Clear resets request headers and reads body into nowhere until completed. // It is implemented to clear the request object between requests func (r *Request) Clear() (err error) { - r.Fragment = "" - r.Query.Set(nil) + r.Path.Fragment = "" + r.Path.Query.Set(nil) r.Ctx = context.Background() r.response = r.response.Clear() diff --git a/indi.go b/indi.go index f6c732c4..87098ecb 100644 --- a/indi.go +++ b/indi.go @@ -111,8 +111,8 @@ func (a *Application) Serve(r router.Router, optionalSettings ...settings.Settin hdrs := headers.NewHeaders(make(map[string][]string, s.Headers.Number.Default)) response := http.NewResponse() bodyReader := http1.NewBodyReader(client, s.Body) - request := http.NewRequest(hdrs, q, response, conn, bodyReader, s.URL.Params.DisableMapClear) - request.Path.Params = make(http.Params) + params := make(http.Params) + request := http.NewRequest(hdrs, q, response, conn, bodyReader, params, s.URL.Params.DisableMapClear) startLineBuff := make([]byte, s.URL.MaxLength) httpParser := http1.NewHTTPRequestsParser( diff --git a/indigo_test.go b/indigo_test.go index 5a45ae33..117e2d28 100644 --- a/indigo_test.go +++ b/indigo_test.go @@ -78,9 +78,9 @@ func getStaticRouter(t *testing.T) router.Router { r.Get("/simple-get", func(request *http.Request) http.Response { require.Equal(t, methods.GET, request.Method) - _, err := request.Query.Get("some non-existing query key") + _, err := request.Path.Query.Get("some non-existing query key") require.Error(t, err) - require.Empty(t, request.Fragment) + require.Empty(t, request.Path.Fragment) require.Equal(t, proto.HTTP11, request.Proto) return http.RespondTo(request) @@ -105,7 +105,7 @@ func getStaticRouter(t *testing.T) router.Router { with := r.Group("/with-") with.Get("query", func(request *http.Request) http.Response { - value, err := request.Query.Get(testQueryKey) + value, err := request.Path.Query.Get(testQueryKey) require.NoError(t, err) require.Equal(t, testQueryValue, string(value)) diff --git a/internal/parser/http1/body_test.go b/internal/parser/http1/body_test.go index 09ed8fb3..6dd01372 100644 --- a/internal/parser/http1/body_test.go +++ b/internal/parser/http1/body_test.go @@ -40,7 +40,7 @@ func getRequestWithReader(chunked bool, body ...[]byte) (*http.Request, http.Bod } request := http.NewRequest( - hdrs, query.Query{}, http.NewResponse(), dummy.NewNopConn(), reader, false, + hdrs, query.Query{}, http.NewResponse(), dummy.NewNopConn(), reader, nil, false, ) request.ContentLength = contentLength request.IsChunked = chunked diff --git a/internal/parser/http1/requestsparser.go b/internal/parser/http1/requestsparser.go index 2de4f281..0cf9d5bd 100644 --- a/internal/parser/http1/requestsparser.go +++ b/internal/parser/http1/requestsparser.go @@ -260,7 +260,7 @@ query: for i := range data { switch data[i] { case ' ': - p.request.Query.Set(p.startLineBuff[p.begin:p.pointer]) + p.request.Path.Query.Set(p.startLineBuff[p.begin:p.pointer]) data = data[i+1:] p.state = eProto goto proto @@ -330,7 +330,7 @@ fragment: for i := range data { switch data[i] { case ' ': - p.request.Fragment = internal.B2S(p.startLineBuff[p.begin:p.pointer]) + p.request.Path.Fragment = internal.B2S(p.startLineBuff[p.begin:p.pointer]) data = data[i+1:] p.state = eProto goto proto diff --git a/internal/parser/http1/requestsparser_test.go b/internal/parser/http1/requestsparser_test.go index ca0318a2..0cb9a8d0 100644 --- a/internal/parser/http1/requestsparser_test.go +++ b/internal/parser/http1/requestsparser_test.go @@ -51,7 +51,7 @@ func getParser() (httpparser.HTTPRequestsParser, *http.Request) { body := NewBodyReader(dummy.NewNopClient(), s.Body) request := http.NewRequest( headers.NewHeaders(nil), query.Query{}, http.NewResponse(), dummy.NewNopConn(), body, - false, + nil, false, ) startLineBuff := make([]byte, s.URL.MaxLength) @@ -312,7 +312,7 @@ func TestHttpRequestsParser_ParsePOST(t *testing.T) { } compareRequests(t, wanted, request) - require.Equal(t, "hel lo=wor ld", string(request.Query.Raw())) + require.Equal(t, "hel lo=wor ld", string(request.Path.Query.Raw())) require.NoError(t, request.Clear()) parser.Release() }) diff --git a/internal/render/engine_test.go b/internal/render/engine_test.go index 86ca1c95..590cb7cb 100644 --- a/internal/render/engine_test.go +++ b/internal/render/engine_test.go @@ -31,7 +31,7 @@ func newRequest() *http.Request { dummy.NewNopClient(), settings.Default().Body, ), - false, + nil, false, ) } diff --git a/internal/render/enginebench_test.go b/internal/render/enginebench_test.go index 1b718528..45d0034c 100644 --- a/internal/render/enginebench_test.go +++ b/internal/render/enginebench_test.go @@ -47,7 +47,8 @@ func BenchmarkRenderer_Response(b *testing.B) { response := http.NewResponse() bodyReader := http1.NewBodyReader(dummy.NewNopClient(), settings.Default().Body) request := http.NewRequest( - hdrs, query.NewQuery(nil), http.NewResponse(), dummy.NewNopConn(), bodyReader, false, + hdrs, query.NewQuery(nil), http.NewResponse(), dummy.NewNopConn(), bodyReader, + nil, false, ) b.Run("DefaultResponse_NoDefHeaders", func(b *testing.B) { diff --git a/internal/server/http/http_bench_test.go b/internal/server/http/http_bench_test.go index b644209d..0366bbba 100644 --- a/internal/server/http/http_bench_test.go +++ b/internal/server/http/http_bench_test.go @@ -120,7 +120,7 @@ func BenchmarkIndigo(b *testing.B) { bodyReader := http1.NewBodyReader(dummy.NewNopClient(), s.Body) hdrs := headers.NewHeaders(make(map[string][]string, 10)) request := http.NewRequest( - hdrs, q, http.NewResponse(), dummy.NewNopConn(), bodyReader, false, + hdrs, q, http.NewResponse(), dummy.NewNopConn(), bodyReader, nil, false, ) keyAllocator := alloc.NewAllocator( s.Headers.MaxKeyLength*s.Headers.Number.Default, diff --git a/router/inbuilt/inbuiltrouter_middlewares_test.go b/router/inbuilt/inbuiltrouter_middlewares_test.go index 0458b3bb..85a708bf 100644 --- a/router/inbuilt/inbuiltrouter_middlewares_test.go +++ b/router/inbuilt/inbuiltrouter_middlewares_test.go @@ -115,7 +115,7 @@ func getRequest() *http.Request { return http.NewRequest( headers.NewHeaders(nil), q, http.NewResponse(), dummy.NewNopConn(), bodyReader, - false, + nil, false, ) } diff --git a/router/inbuilt/obtainer/obtainer_test.go b/router/inbuilt/obtainer/obtainer_test.go index 73877f4b..68b0daee 100644 --- a/router/inbuilt/obtainer/obtainer_test.go +++ b/router/inbuilt/obtainer/obtainer_test.go @@ -26,7 +26,7 @@ func newRequest(path string, method methods.Method) *http.Request { hdrs := headers.NewHeaders(make(map[string][]string)) bodyReader := http1.NewBodyReader(dummy.NewNopClient(), settings.Default().Body) request := http.NewRequest( - hdrs, query.Query{}, http.NewResponse(), dummy.NewNopConn(), bodyReader, false, + hdrs, query.Query{}, http.NewResponse(), dummy.NewNopConn(), bodyReader, nil, false, ) request.Path.String = path request.Method = method diff --git a/router/inbuilt/trace.go b/router/inbuilt/trace.go index 8402c0c7..9d904167 100644 --- a/router/inbuilt/trace.go +++ b/router/inbuilt/trace.go @@ -35,14 +35,14 @@ func renderHTTPRequest(request *http.Request, buff []byte) []byte { func requestURI(request *http.Request, buff []byte) []byte { buff = append(buff, request.Path.String...) - if query := request.Query.Raw(); len(query) > 0 { + if query := request.Path.Query.Raw(); len(query) > 0 { buff = append(buff, '?') buff = append(buff, query...) } - if len(request.Fragment) > 0 { + if len(request.Path.Fragment) > 0 { buff = append(buff, '#') - buff = append(buff, request.Fragment...) + buff = append(buff, request.Path.Fragment...) } return buff