mirror of
https://github.com/soypat/lneto.git
synced 2026-08-22 15:39:06 +00:00
add query handling
This commit is contained in:
@@ -3,6 +3,8 @@ package httphi
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// benchConn replays a fixed request and discards the response. It allocates
|
// benchConn replays a fixed request and discards the response. It allocates
|
||||||
@@ -48,17 +50,23 @@ func benchExchange(b *testing.B, conn conn) *Exchange {
|
|||||||
|
|
||||||
// BenchmarkHandle measures a whole exchange: read, parse, mux and respond.
|
// BenchmarkHandle measures a whole exchange: read, parse, mux and respond.
|
||||||
func BenchmarkHandle(b *testing.B) {
|
func BenchmarkHandle(b *testing.B) {
|
||||||
|
expect := []byte("123")
|
||||||
|
buf := make([]byte, 64)
|
||||||
for _, bb := range []struct {
|
for _, bb := range []struct {
|
||||||
name string
|
name string
|
||||||
request string
|
request string
|
||||||
handler HandlerFunc
|
handler HandlerFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "GETWithHeaders",
|
name: "GETWithHeadersAndQuery",
|
||||||
request: "GET / HTTP/1.1\r\nHost: tinygo.org\r\nUser-Agent: bench\r\nAccept: */*\r\nConnection: close\r\n\r\n",
|
request: "GET /?abc=123 HTTP/1.1\r\nHost: tinygo.org\r\nUser-Agent: bench\r\nAccept: */*\r\nConnection: close\r\n\r\n",
|
||||||
handler: func(ex *Exchange) {
|
handler: func(ex *Exchange) {
|
||||||
ex.SetHeader("Content-Type", "text/plain")
|
ex.SetHeader("Content-Type", "text/plain")
|
||||||
ex.SetHeaderInt("Content-Length", int64(len(benchBody)), 10)
|
ex.SetHeaderInt("Content-Length", int64(len(benchBody)), 10)
|
||||||
|
data, present := ex.AppendQuery(buf[:0], "abc", true)
|
||||||
|
if !present || !internal.BytesEqual(data, expect) {
|
||||||
|
panic("invalid result")
|
||||||
|
}
|
||||||
ex.Write(benchBody)
|
ex.Write(benchBody)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -78,7 +86,7 @@ func BenchmarkHandle(b *testing.B) {
|
|||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
b.SetBytes(int64(len(bb.request)))
|
b.SetBytes(int64(len(bb.request)))
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for b.Loop() {
|
for i := 0; i < b.N; i++ {
|
||||||
conn.rewind()
|
conn.rewind()
|
||||||
exch.Release()
|
exch.Release()
|
||||||
exch.Acquire(conn)
|
exch.Acquire(conn)
|
||||||
|
|||||||
@@ -190,7 +190,8 @@ func TestHandleRequestFields(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
var gotMethod, gotURI, gotHost string
|
var gotMethod, gotURI, gotHost string
|
||||||
var sm MuxSlice
|
var sm MuxSlice
|
||||||
sm.Handle(test.wantURI, func(ex *Exchange) {
|
route, _, _ := strings.Cut(test.wantURI, "?") // Mux matches on path.
|
||||||
|
sm.Handle(route, func(ex *Exchange) {
|
||||||
gotMethod = string(ex.RequestMethod())
|
gotMethod = string(ex.RequestMethod())
|
||||||
gotURI = string(ex.RequestURI())
|
gotURI = string(ex.RequestURI())
|
||||||
gotHost = string(ex.RequestHeader("Host"))
|
gotHost = string(ex.RequestHeader("Host"))
|
||||||
@@ -453,3 +454,100 @@ func TestExchangeSetHeaderIntNoAlloc(t *testing.T) {
|
|||||||
t.Errorf("SetHeaderInt allocated %v times, want 0", allocs)
|
t.Errorf("SetHeaderInt allocated %v times, want 0", allocs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Mux matches on the request path: a query string must not defeat routing.
|
||||||
|
func TestHandleMuxOnPath(t *testing.T) {
|
||||||
|
var sm MuxSlice
|
||||||
|
var gotPath, gotQuery string
|
||||||
|
sm.Handle("GET /search", func(ex *Exchange) {
|
||||||
|
gotPath = string(ex.RequestPath())
|
||||||
|
ex.ForEachQueryRaw(func(rawkey, rawval []byte) bool {
|
||||||
|
gotQuery += string(rawkey) + "=" + string(rawval) + ";"
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
ex.WriteHeader(200)
|
||||||
|
})
|
||||||
|
conn := serve(t, "GET /search?q=go&n=1 HTTP/1.1\r\nHost: h\r\n\r\n", &sm)
|
||||||
|
|
||||||
|
if gotPath != "/search" {
|
||||||
|
t.Errorf("want path %q, got %q", "/search", gotPath)
|
||||||
|
}
|
||||||
|
if gotQuery != "q=go;n=1;" {
|
||||||
|
t.Errorf("want query %q, got %q", "q=go;n=1;", gotQuery)
|
||||||
|
}
|
||||||
|
if got := conn.ViewWritten(); !strings.HasPrefix(got, "HTTP/1.1 200 OK\r\n") {
|
||||||
|
t.Errorf("want the handler to have run, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExchangeAppendQuery(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
uri string
|
||||||
|
key string
|
||||||
|
decoded bool
|
||||||
|
want string
|
||||||
|
wantPresent bool
|
||||||
|
}{
|
||||||
|
{uri: "/x?q=go", key: "q", want: "go", wantPresent: true},
|
||||||
|
{uri: "/x?q=go&n=1", key: "n", want: "1", wantPresent: true},
|
||||||
|
{uri: "/x?a=1&a=2", key: "a", want: "1", wantPresent: true}, // First match wins.
|
||||||
|
{uri: "/x?q=go", key: "nope", want: "", wantPresent: false}, // Absent.
|
||||||
|
{uri: "/x", key: "q", want: "", wantPresent: false}, // No query at all.
|
||||||
|
{uri: "/x?debug&q=go", key: "debug", want: "", wantPresent: true}, // Flag: present, no value.
|
||||||
|
{uri: "/x?q=", key: "q", want: "", wantPresent: true}, // Present, empty.
|
||||||
|
// Decoding is opt-in and applies to the value only.
|
||||||
|
{uri: "/x?q=hello%20world", key: "q", want: "hello%20world", wantPresent: true},
|
||||||
|
{uri: "/x?q=hello%20world", key: "q", decoded: true, want: "hello world", wantPresent: true},
|
||||||
|
{uri: "/x?q=a+b", key: "q", want: "a+b", wantPresent: true},
|
||||||
|
{uri: "/x?q=a+b", key: "q", decoded: true, want: "a b", wantPresent: true},
|
||||||
|
// Keys are matched decoded: "a b" cannot appear raw.
|
||||||
|
{uri: "/x?a%20b=c", key: "a b", want: "c", wantPresent: true},
|
||||||
|
{uri: "/x?a+b=c", key: "a b", want: "c", wantPresent: true},
|
||||||
|
// Malformed escapes: a bad key is skipped, a bad value is not returned.
|
||||||
|
{uri: "/x?%zz=1&q=go", key: "q", want: "go", wantPresent: true},
|
||||||
|
{uri: "/x?q=%zz", key: "q", decoded: true, want: "", wantPresent: false},
|
||||||
|
{uri: "/x?q=%zz", key: "q", want: "%zz", wantPresent: true}, // Undecoded, passed through.
|
||||||
|
} {
|
||||||
|
var sm MuxSlice
|
||||||
|
var got string
|
||||||
|
var present bool
|
||||||
|
sm.Handle("/x", func(ex *Exchange) {
|
||||||
|
var value []byte
|
||||||
|
value, present = ex.AppendQuery(nil, test.key, test.decoded)
|
||||||
|
got = string(value)
|
||||||
|
})
|
||||||
|
serve(t, "GET "+test.uri+" HTTP/1.1\r\nHost: h\r\n\r\n", &sm)
|
||||||
|
|
||||||
|
if present != test.wantPresent {
|
||||||
|
t.Errorf("%s key %q decoded=%v: want present=%v, got %v", test.uri, test.key, test.decoded, test.wantPresent, present)
|
||||||
|
}
|
||||||
|
if got != test.want {
|
||||||
|
t.Errorf("%s key %q decoded=%v: want %q, got %q", test.uri, test.key, test.decoded, test.want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendQuery appends to dst, leaving what was already there untouched, and
|
||||||
|
// does not allocate when dst has the capacity.
|
||||||
|
func TestExchangeAppendQueryReusesBuffer(t *testing.T) {
|
||||||
|
var sm MuxSlice
|
||||||
|
var got string
|
||||||
|
var allocs float64
|
||||||
|
dst := make([]byte, 0, 64)
|
||||||
|
sm.Handle("/x", func(ex *Exchange) {
|
||||||
|
var value []byte
|
||||||
|
allocs = testing.AllocsPerRun(50, func() {
|
||||||
|
value, _ = ex.AppendQuery(dst[:len("prefix:")], "q", true)
|
||||||
|
})
|
||||||
|
got = string(value) // Conversion allocates, keep it out of the measurement.
|
||||||
|
})
|
||||||
|
copy(dst[:cap(dst)], "prefix:")
|
||||||
|
serve(t, "GET /x?q=hello%20world HTTP/1.1\r\nHost: h\r\n\r\n", &sm)
|
||||||
|
|
||||||
|
if got != "prefix:hello world" {
|
||||||
|
t.Errorf("want %q, got %q", "prefix:hello world", got)
|
||||||
|
}
|
||||||
|
if allocs != 0 {
|
||||||
|
t.Errorf("AppendQuery allocated %v times into a buffer with capacity, want 0", allocs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+66
-3
@@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -215,10 +216,10 @@ func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error {
|
|||||||
exch.WriteHeader(int(StatusBadRequest))
|
exch.WriteHeader(int(StatusBadRequest))
|
||||||
return errNoRequestProto
|
return errNoRequestProto
|
||||||
}
|
}
|
||||||
// Mux URI.
|
// Mux on the request path: the query string is the handler's business.
|
||||||
uri := reqhdr.RequestURI()
|
path := reqhdr.RequestPath()
|
||||||
meth := reqhdr.Method()
|
meth := reqhdr.Method()
|
||||||
handler := mux.LookupHandler(MethodFromBytes(meth), uri)
|
handler := mux.LookupHandler(MethodFromBytes(meth), path)
|
||||||
if handler != nil {
|
if handler != nil {
|
||||||
handler(exch)
|
handler(exch)
|
||||||
exch.FlushHeader()
|
exch.FlushHeader()
|
||||||
@@ -589,6 +590,68 @@ func (exch *Exchange) RequestURI() []byte {
|
|||||||
return exch.RequestHeaderRaw().RequestURI()
|
return exch.RequestHeaderRaw().RequestURI()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestPath returns the request URI up to the query string. This is what the
|
||||||
|
// [Mux] matches on, i.e: "/search" for a request to "/search?q=go".
|
||||||
|
func (exch *Exchange) RequestPath() []byte {
|
||||||
|
return exch.RequestHeaderRaw().RequestPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForEachQueryRaw iterates over the request's query string key-value pairs as
|
||||||
|
// they appear on the wire. See [httpraw.Header.ForEachQuery].
|
||||||
|
func (exch *Exchange) ForEachQueryRaw(fn func(rawkey, rawval []byte) bool) {
|
||||||
|
exch.RequestHeaderRaw().ForEachQuery(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendQuery appends the value of the first query parameter matching key to
|
||||||
|
// dst and reports whether the parameter was present. A parameter with no value
|
||||||
|
// ("?debug") and one with an empty value ("?debug=") are both present with
|
||||||
|
// nothing appended.
|
||||||
|
//
|
||||||
|
// Keys are matched decoded, so key "a b" finds "a%20b" and "a+b". Values are
|
||||||
|
// appended raw unless decoded is set, in which case percent escapes and '+' are
|
||||||
|
// decoded. A parameter whose value fails to decode is reported absent, and a
|
||||||
|
// parameter whose key fails to decode is skipped.
|
||||||
|
//
|
||||||
|
// dst doubles as scratch space for decoding candidate keys, so AppendQuery only
|
||||||
|
// allocates when dst lacks the capacity to hold the longest key it inspects.
|
||||||
|
func (exch *Exchange) AppendQuery(dst []byte, key string, decoded bool) (valueAppended []byte, present bool) {
|
||||||
|
const plusAsSpace = true // Query strings are form encoded, unlike paths.
|
||||||
|
base := len(dst)
|
||||||
|
valueAppended = dst
|
||||||
|
exch.ForEachQueryRaw(func(rawkey, rawval []byte) bool {
|
||||||
|
if b2s(rawkey) != key {
|
||||||
|
// Key may be encoded: decode it over dst's free space and compare.
|
||||||
|
// A decoded key cannot appear raw, so this cannot alias a real key.
|
||||||
|
valueAppended = slices.Grow(valueAppended, len(rawkey))
|
||||||
|
scratch := valueAppended[base : base+len(rawkey)]
|
||||||
|
n, err := httpraw.CopyDecodedPercentURL(scratch, rawkey, plusAsSpace)
|
||||||
|
if err != nil || b2s(scratch[:n]) != key {
|
||||||
|
return true // Malformed or different key, keep looking.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
present = true
|
||||||
|
if len(rawval) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
valueAppended = slices.Grow(valueAppended, len(rawval))
|
||||||
|
if !decoded {
|
||||||
|
valueAppended = append(valueAppended[:base], rawval...)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n, err := httpraw.CopyDecodedPercentURL(valueAppended[base:base+len(rawval)], rawval, plusAsSpace)
|
||||||
|
if err != nil {
|
||||||
|
present = false // Malformed value, do not hand back half a decode.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
valueAppended = valueAppended[:base+n]
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if !present {
|
||||||
|
valueAppended = valueAppended[:base]
|
||||||
|
}
|
||||||
|
return valueAppended, present
|
||||||
|
}
|
||||||
|
|
||||||
func (exch *Exchange) RequestMethod() []byte {
|
func (exch *Exchange) RequestMethod() []byte {
|
||||||
return exch.RequestHeaderRaw().Method()
|
return exch.RequestHeaderRaw().Method()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
package httphi
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// Scratch benchmarks isolating query handling. Delete after diagnosis.
|
||||||
|
|
||||||
|
const scratchRequest = "GET /?abc=123 HTTP/1.1\r\nHost: tinygo.org\r\nUser-Agent: bench\r\nAccept: */*\r\nConnection: close\r\n\r\n"
|
||||||
|
|
||||||
|
func scratchExchange(b *testing.B) (*Exchange, *benchConn, *MuxSlice) {
|
||||||
|
b.Helper()
|
||||||
|
conn := &benchConn{request: scratchRequest}
|
||||||
|
exch := benchExchange(b, conn)
|
||||||
|
mux := new(MuxSlice)
|
||||||
|
return exch, conn, mux
|
||||||
|
}
|
||||||
|
|
||||||
|
// A: whole exchange, handler writes body only. No query touched.
|
||||||
|
func BenchmarkScratchBodyOnly(b *testing.B) {
|
||||||
|
exch, conn, mux := scratchExchange(b)
|
||||||
|
mux.Handle("GET /", func(ex *Exchange) { ex.Write(benchBody) })
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
conn.rewind()
|
||||||
|
exch.Release()
|
||||||
|
exch.Acquire(conn)
|
||||||
|
Handle(exch, mux, nopBackoff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// B: same plus header writes. Isolates SetHeader/SetHeaderInt.
|
||||||
|
func BenchmarkScratchHeaders(b *testing.B) {
|
||||||
|
exch, conn, mux := scratchExchange(b)
|
||||||
|
mux.Handle("GET /", func(ex *Exchange) {
|
||||||
|
ex.SetHeader("Content-Type", "text/plain")
|
||||||
|
ex.SetHeaderInt("Content-Length", int64(len(benchBody)), 10)
|
||||||
|
ex.Write(benchBody)
|
||||||
|
})
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
conn.rewind()
|
||||||
|
exch.Release()
|
||||||
|
exch.Acquire(conn)
|
||||||
|
Handle(exch, mux, nopBackoff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C: iteration only, closure captures one counter.
|
||||||
|
func BenchmarkScratchForEachQuery(b *testing.B) {
|
||||||
|
exch, conn, mux := scratchExchange(b)
|
||||||
|
var seen int
|
||||||
|
mux.Handle("GET /", func(ex *Exchange) {
|
||||||
|
ex.ForEachQueryRaw(func(rawkey, rawval []byte) bool {
|
||||||
|
seen += len(rawkey)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
conn.rewind()
|
||||||
|
exch.Release()
|
||||||
|
exch.Acquire(conn)
|
||||||
|
Handle(exch, mux, nopBackoff)
|
||||||
|
}
|
||||||
|
_ = seen
|
||||||
|
}
|
||||||
|
|
||||||
|
// C2: closure that reassigns a captured slice, exactly AppendQuery's shape.
|
||||||
|
func BenchmarkScratchClosureSlice(b *testing.B) {
|
||||||
|
exch, conn, mux := scratchExchange(b)
|
||||||
|
buf := make([]byte, 64)
|
||||||
|
mux.Handle("GET /", func(ex *Exchange) {
|
||||||
|
out := buf[:0]
|
||||||
|
found := false
|
||||||
|
ex.ForEachQueryRaw(func(rawkey, rawval []byte) bool {
|
||||||
|
if b2s(rawkey) != "abc" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
out = append(out, rawval...)
|
||||||
|
found = true
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
_ = found
|
||||||
|
_ = out
|
||||||
|
})
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
conn.rewind()
|
||||||
|
exch.Release()
|
||||||
|
exch.Acquire(conn)
|
||||||
|
Handle(exch, mux, nopBackoff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// C3: same but the captured slice is only read, never reassigned.
|
||||||
|
func BenchmarkScratchClosureNoReassign(b *testing.B) {
|
||||||
|
exch, conn, mux := scratchExchange(b)
|
||||||
|
buf := make([]byte, 64)
|
||||||
|
mux.Handle("GET /", func(ex *Exchange) {
|
||||||
|
n := 0
|
||||||
|
ex.ForEachQueryRaw(func(rawkey, rawval []byte) bool {
|
||||||
|
n += copy(buf, rawval)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
_ = n
|
||||||
|
})
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
conn.rewind()
|
||||||
|
exch.Release()
|
||||||
|
exch.Acquire(conn)
|
||||||
|
Handle(exch, mux, nopBackoff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// D: AppendQuery raw, into a buffer with capacity.
|
||||||
|
func BenchmarkScratchAppendQueryRaw(b *testing.B) {
|
||||||
|
exch, conn, mux := scratchExchange(b)
|
||||||
|
buf := make([]byte, 64)
|
||||||
|
mux.Handle("GET /", func(ex *Exchange) {
|
||||||
|
ex.AppendQuery(buf[:0], "abc", false)
|
||||||
|
})
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
conn.rewind()
|
||||||
|
exch.Release()
|
||||||
|
exch.Acquire(conn)
|
||||||
|
Handle(exch, mux, nopBackoff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// E: AppendQuery decoded.
|
||||||
|
func BenchmarkScratchAppendQueryDecoded(b *testing.B) {
|
||||||
|
exch, conn, mux := scratchExchange(b)
|
||||||
|
buf := make([]byte, 64)
|
||||||
|
mux.Handle("GET /", func(ex *Exchange) {
|
||||||
|
ex.AppendQuery(buf[:0], "abc", true)
|
||||||
|
})
|
||||||
|
b.ReportAllocs()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
conn.rewind()
|
||||||
|
exch.Release()
|
||||||
|
exch.Acquire(conn)
|
||||||
|
Handle(exch, mux, nopBackoff)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -361,6 +361,53 @@ func (h *Header) RequestURI() []byte {
|
|||||||
return h.getNonEmptyValue(h.requestURI)
|
return h.getNonEmptyValue(h.requestURI)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestPath returns the request URI up to the query string, i.e: "/search"
|
||||||
|
// for "/search?q=go". Returns the whole URI if it contains no query string.
|
||||||
|
func (h *Header) RequestPath() []byte {
|
||||||
|
uri := h.RequestURI()
|
||||||
|
query := bytes.IndexByte(uri, '?')
|
||||||
|
if query < 0 {
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
return uri[:query]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForEachQuery iterates over the request URI's query string key-value pairs as
|
||||||
|
// they appear on the wire, percent-encoded and with '+' undecoded. fn returns
|
||||||
|
// true to continue iterating, false to stop.
|
||||||
|
//
|
||||||
|
// A pair with no '=' yields a nil value, i.e: "debug" in "?debug&q=go", which
|
||||||
|
// distinguishes it from "?debug=" where the value is present and empty. Empty
|
||||||
|
// sequences are skipped, so "?&&q=go&" yields a single pair. Only '&' separates
|
||||||
|
// pairs and only the first '=' splits a pair.
|
||||||
|
func (h *Header) ForEachQuery(fn func(rawkey, rawval []byte) bool) {
|
||||||
|
uri := h.RequestURI()
|
||||||
|
start := bytes.IndexByte(uri, '?')
|
||||||
|
if start < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
query := uri[start+1:]
|
||||||
|
for len(query) > 0 {
|
||||||
|
pair := query
|
||||||
|
amp := bytes.IndexByte(query, '&')
|
||||||
|
if amp >= 0 {
|
||||||
|
pair, query = query[:amp], query[amp+1:]
|
||||||
|
} else {
|
||||||
|
query = nil
|
||||||
|
}
|
||||||
|
if len(pair) == 0 {
|
||||||
|
continue // Empty sequence, see WHATWG URL urlencoded parsing.
|
||||||
|
}
|
||||||
|
key, value := pair, []byte(nil)
|
||||||
|
if eq := bytes.IndexByte(pair, '='); eq >= 0 {
|
||||||
|
key, value = pair[:eq], pair[eq+1:]
|
||||||
|
}
|
||||||
|
if !fn(key, value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Protocol returns the request header's HTTP protocol. Usually "HTTP/1.1".
|
// Protocol returns the request header's HTTP protocol. Usually "HTTP/1.1".
|
||||||
func (h *Header) Protocol() []byte {
|
func (h *Header) Protocol() []byte {
|
||||||
return h.getNonEmptyValue(h.proto)
|
return h.getNonEmptyValue(h.proto)
|
||||||
|
|||||||
@@ -151,6 +151,85 @@ func strSameSite(mode http.SameSite) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHeaderRequestPath(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
uri string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{uri: "/", want: "/"},
|
||||||
|
{uri: "/search?q=go", want: "/search"},
|
||||||
|
{uri: "/search?", want: "/search"},
|
||||||
|
{uri: "/a/b/c?x=1&y=2", want: "/a/b/c"},
|
||||||
|
{uri: "/?q=go", want: "/"},
|
||||||
|
} {
|
||||||
|
var h Header
|
||||||
|
err := h.ParseBytes(false, []byte("GET "+test.uri+" HTTP/1.1\r\nHost: h\r\n\r\n"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := string(h.RequestPath()); got != test.want {
|
||||||
|
t.Errorf("uri %q: want path %q, got %q", test.uri, test.want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHeaderForEachQuery(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
uri string
|
||||||
|
want string // "key=value" pairs joined by '|'; nil value shown as "key".
|
||||||
|
}{
|
||||||
|
{uri: "/", want: ""},
|
||||||
|
{uri: "/x?", want: ""},
|
||||||
|
{uri: "/x?q=go", want: "q=go"},
|
||||||
|
{uri: "/x?q=go&n=1", want: "q=go|n=1"},
|
||||||
|
{uri: "/x?debug&q=go", want: "debug|q=go"}, // No '=' yields a nil value.
|
||||||
|
{uri: "/x?q=", want: "q="}, // Empty but present value.
|
||||||
|
{uri: "/x?&&q=go&", want: "q=go"}, // Empty sequences skipped.
|
||||||
|
{uri: "/x?=v", want: "=v"}, // Empty name is kept.
|
||||||
|
{uri: "/x?a=1&a=2", want: "a=1|a=2"}, // Duplicates all yielded.
|
||||||
|
{uri: "/x?a%20b=c%20d", want: "a%20b=c%20d"}, // Raw, undecoded.
|
||||||
|
{uri: "/x?a=b=c", want: "a=b=c"}, // Only first '=' splits.
|
||||||
|
} {
|
||||||
|
var h Header
|
||||||
|
err := h.ParseBytes(false, []byte("GET "+test.uri+" HTTP/1.1\r\nHost: h\r\n\r\n"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var got []byte
|
||||||
|
h.ForEachQuery(func(rawkey, rawval []byte) bool {
|
||||||
|
if len(got) > 0 {
|
||||||
|
got = append(got, '|')
|
||||||
|
}
|
||||||
|
got = append(got, rawkey...)
|
||||||
|
if rawval != nil {
|
||||||
|
got = append(got, '=')
|
||||||
|
got = append(got, rawval...)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if string(got) != test.want {
|
||||||
|
t.Errorf("uri %q: want %q, got %q", test.uri, test.want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn returning false stops iteration.
|
||||||
|
func TestHeaderForEachQueryStop(t *testing.T) {
|
||||||
|
var h Header
|
||||||
|
err := h.ParseBytes(false, []byte("GET /x?a=1&b=2&c=3 HTTP/1.1\r\nHost: h\r\n\r\n"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
visited := 0
|
||||||
|
h.ForEachQuery(func(rawkey, rawval []byte) bool {
|
||||||
|
visited++
|
||||||
|
return string(rawkey) != "b"
|
||||||
|
})
|
||||||
|
if visited != 2 {
|
||||||
|
t.Errorf("want iteration stopped after 2 pairs, visited %d", visited)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHeaderNormalizeKey(t *testing.T) {
|
func TestHeaderNormalizeKey(t *testing.T) {
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
key string
|
key string
|
||||||
|
|||||||
Reference in New Issue
Block a user