add query handling

This commit is contained in:
Patricio Whittingslow
2026-07-25 22:45:59 -03:00
parent 268c7a6d5f
commit d5a5e44ddf
6 changed files with 447 additions and 7 deletions
+47
View File
@@ -361,6 +361,53 @@ func (h *Header) RequestURI() []byte {
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".
func (h *Header) Protocol() []byte {
return h.getNonEmptyValue(h.proto)
+79
View File
@@ -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) {
var tests = []struct {
key string