document all the things; improve req Query semantics; add Form.EnableBufferGrowth

This commit is contained in:
Patricio Whittingslow
2026-07-29 17:01:03 -03:00
parent 29fcec9cb7
commit 17bee3ed48
11 changed files with 183 additions and 111 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ func BenchmarkHandle(b *testing.B) {
handler: func(ex *Exchange) {
ex.StageHeader("Content-Type", "text/plain")
ex.StageHeaderInt("Content-Length", int64(len(benchBody)), 10)
data, present := ex.AppendQuery(buf[:0], "abc", true)
data, present := ex.RequestQueryAppend(buf[:0], "abc", true)
if !present || !internal.BytesEqual(data, expect) {
panic("invalid result")
}
+7 -3
View File
@@ -62,15 +62,19 @@ func ExampleMuxSlice_query_forms_multipart() {
mux.Handle("/users/{id}", func(ex *httphi.Exchange) {
userID := ex.PathValue("id")
fmt.Printf("someone requested data for user %s", userID)
fmt.Printf("someone requested data for user %s\n", userID)
})
mux.Handle("/query", func(ex *httphi.Exchange) {
// query parameter in URL.
const decodeQuery = true
const queryKey = "search"
queryValue, present := ex.AppendQuery(nil, queryKey, decodeQuery)
fmt.Printf("got query=%v %s=%s", present, queryKey, queryValue)
valueRaw, present := ex.RequestQueryValue(queryKey)
if !present {
return
}
valueDecoded, present := ex.RequestQueryAppend(nil, queryKey, decodeQuery)
fmt.Printf("got query=%v %s=%s (raw:%s)\n", present, queryKey, valueDecoded, valueRaw)
})
mux.Handle("GET /form", func(ex *httphi.Exchange) {
+56 -39
View File
@@ -49,11 +49,24 @@ type Exchange struct {
readErr error
}
// ExchangeConfig is the memory an [Exchange] is fixed to for the rest of its
// life by [Exchange.Configure]. A [Router] derives one per exchange from its
// [RouterConfig], which is what bounds the router's memory.
type ExchangeConfig struct {
RawBuf []byte
RequestBufferLim int
NumHeaderKVCap int
// RawBuf is the single buffer holding the request header, the response
// header and any surplus body. See [Exchange.UnsafeRawBuffer].
RawBuf []byte
// RequestBufferLim reserves the first bytes of RawBuf for the request
// header, the rest being the response. Configure panics if it exceeds RawBuf.
RequestBufferLim int
// NumHeaderKVCap is how many request header fields may be parsed. A request
// carrying more is answered 431, see [httpraw.ErrHeaderTooMany].
NumHeaderKVCap int
// NormalizeOutgoingKeys normalizes staged response header keys as they are
// written, i.e: "content-type" becomes "Content-Type".
NormalizeOutgoingKeys bool
// NoRequestBufferGrowth holds the request header to RequestBufferLim rather
// than growing it. A header outgrowing it is answered 431, see [httpraw.ErrBufferExhausted].
NoRequestBufferGrowth bool
// MaxPathValues is how many wildcards a single pattern may bind, read back with
// [Exchange.PathValue]. A pattern with more never matches, see [SetPathValues].
@@ -443,7 +456,7 @@ func (exch *Exchange) RequestContentLength() (_ int64, present bool, _ error) {
// left as they arrived, call [httpraw.Form.Decode] to decode them in place.
//
// Unlike http.Request.ParseForm the query string is not folded in, reach it with
// [Exchange.RequestQuery] or [Exchange.AppendQuery]. The body is consumed, so
// [Exchange.RequestQuery] or [Exchange.RequestQueryAppend]. The body is consumed, so
// call this before [Exchange.ReadBody].
//
// A request with no Content-Length has no body, RFC 9112 6.3, and yields an
@@ -615,47 +628,51 @@ func (exch *Exchange) RequestQuery() []byte {
return exch.RequestHeaderRaw().RequestQuery()
}
// 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.
// RequestQueryValue returns an undecoded view of the first query parameter
// matching key and reports whether it was present. Keys are matched decoded, so
// key "a b" finds "a%20b" and "a+b"; a parameter whose key is a malformed
// escape is skipped. A parameter with no value ("?debug") and one with an empty
// value ("?debug=") are both present with a zero length view.
//
// 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) {
// The view aliases the request buffer, so copy it to outlive the handler or use
// [Exchange.RequestQueryAppend] to decode it out.
func (exch *Exchange) RequestQueryValue(key string) (rawValue []byte, present bool) {
const plusAsSpace = true // Query strings are form encoded, unlike paths.
base := len(dst)
rawkey, rawval, rest := httpraw.NextQueryPair(exch.RequestQuery())
for ; rawkey != nil; rawkey, rawval, rest = httpraw.NextQueryPair(rest) {
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.
dst = slices.Grow(dst, len(rawkey))
scratch := dst[base : base+len(rawkey)]
n, err := httpraw.CopyDecodedPercentURL(scratch, rawkey, plusAsSpace)
if err != nil || b2s(scratch[:n]) != key {
continue // Malformed or different key, keep looking.
}
// Compare raw first: a key needing no decoding is the common case, and
// the decoding compare walks the key an escape at a time.
if b2s(rawkey) == key || httpraw.EqualDecodedPercentURL(rawkey, key, plusAsSpace) {
return rawval, true
}
if len(rawval) == 0 {
return dst[:base], true // Flag or empty value, nothing to append.
}
dst = slices.Grow(dst, len(rawval))
if !decoded {
return append(dst[:base], rawval...), true
}
n, err := httpraw.CopyDecodedPercentURL(dst[base:base+len(rawval)], rawval, plusAsSpace)
if err != nil {
return dst[:base], false // Do not hand back half a decode.
}
return dst[:base+n], true
}
return dst[:base], false
return nil, false
}
// RequestQueryAppend appends the value of the first query parameter matching key to
// dst and reports whether the parameter was present, matching keys as
// [Exchange.RequestQueryValue] does. A parameter with no value ("?debug") and
// one with an empty value ("?debug=") are both present with nothing appended.
//
// 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, dst being left as it was rather than holding half a decode.
func (exch *Exchange) RequestQueryAppend(dst []byte, key string, decoded bool) (valueAppended []byte, present bool) {
const plusAsSpace = true // Query strings are form encoded, unlike paths.
rawval, present := exch.RequestQueryValue(key)
if !present || len(rawval) == 0 {
return dst, present
}
if !decoded {
return append(dst, rawval...), true
}
base := len(dst)
dst = slices.Grow(dst, len(rawval))
n, err := httpraw.CopyDecodedPercentURL(dst[base:base+len(rawval)], rawval, plusAsSpace)
if err != nil {
return dst[:base], false // Do not hand back half a decode.
}
return dst[:base+n], true
}
// RequestMethod returns the request line's method, i.e: "GET". See
+2 -2
View File
@@ -671,7 +671,7 @@ func TestExchangeAppendQuery(t *testing.T) {
var present bool
sm.Handle("/x", func(ex *Exchange) {
var value []byte
value, present = ex.AppendQuery(nil, test.key, test.decoded)
value, present = ex.RequestQueryAppend(nil, test.key, test.decoded)
got = string(value)
})
serve(t, "GET "+test.uri+" HTTP/1.1\r\nHost: h\r\n\r\n", &sm)
@@ -696,7 +696,7 @@ func TestExchangeAppendQueryReusesBuffer(t *testing.T) {
sm.Handle("/x", func(ex *Exchange) {
var value []byte
allocs = testing.AllocsPerRun(50, func() {
value, _ = ex.AppendQuery(dst[:len("prefix:")], "q", true)
value, _ = ex.RequestQueryAppend(dst[:len("prefix:")], "q", true)
})
got = string(value) // Conversion allocates, keep it out of the measurement.
})
+1 -1
View File
@@ -242,7 +242,7 @@ func FuzzQueryAndForm(f *testing.F) {
if n > len(key) {
t.Fatalf("decoding key %q grew it to %d bytes", key, n)
}
if _, present := exch.AppendQuery(nil, string(dec[:n]), decoded); !present {
if _, present := exch.RequestQueryAppend(nil, string(dec[:n]), decoded); !present {
t.Fatalf("query pair %q absent from AppendQuery", key)
}
}