diff --git a/context.go b/context.go index 9be52f0aa..6b8902971 100644 --- a/context.go +++ b/context.go @@ -332,14 +332,23 @@ func (c *Context) QueryParam(name string) string { // unescape are skipped. It avoids allocating the full url.Values map for single-key lookups. func getRawQueryParam(query, name string) string { for query != "" { - var seg string - seg, query, _ = strings.Cut(query, "&") - if seg == "" || strings.Contains(seg, ";") { + // strings.IndexByte instead of strings.Cut skips the call to strings.Index for each segment. + seg := query + if i := strings.IndexByte(query, '&'); i >= 0 { + seg, query = query[:i], query[i+1:] + } else { + query = "" + } + key, value := seg, "" + if i := strings.IndexByte(seg, '='); i >= 0 { + key, value = seg[:i], seg[i+1:] + } + // Unescaping never makes a key longer, so a shorter key cannot match. + if len(key) < len(name) { continue } - key, value, _ := strings.Cut(seg, "=") k, err := url.QueryUnescape(key) - if err != nil || k != name { + if err != nil || k != name || seg == "" || strings.Contains(seg, ";") { continue } v, err := url.QueryUnescape(value) diff --git a/query_fastpath_test.go b/query_fastpath_test.go index ae4ac82e8..420beb38e 100644 --- a/query_fastpath_test.go +++ b/query_fastpath_test.go @@ -50,6 +50,9 @@ func TestGetRawQueryParamMatchesStdlib(t *testing.T) { "q=%E4%B8%AD%E6%96%87", // unicode value "empty=&x=1", "a=1&&b=2", // empty segment + + "na%6De=1&name=2", // escaped key is longer than the name it matches + "names=1&nam=2&name=3", // keys that are longer or shorter than the name } names := []string{"a", "b", "c", "d", "e", "name", "age", "q", "empty", "x", "a=b", "missing", ""}