fix(tree): use url.PathUnescape for path parameters

Replace url.QueryUnescape with url.PathUnescape when decoding
wildcard parameter values in the radix tree. Per RFC 3986, URL
path segments preserve '+' as a literal character; the '+' to
space convention belongs to application/x-www-form-urlencoded,
not to URL paths.

The change affects only configurations using UseEscapedPath or
UseRawPath together with UnescapePathValues=true. The default
routing path is unchanged because URL.Path is already decoded
by net/http upstream of getValue.

Two existing test cases in TestUnescapeParameters previously
asserted the incorrect '+' to space behavior and have been
updated. A new test case verifies that '%2B' (percent-encoded
'+') still decodes to '+'.

Fixes #3850
This commit is contained in:
baltasarblanco 2026-05-28 01:36:50 -03:00
parent 5f4f964325
commit 4f17499690
No known key found for this signature in database
2 changed files with 5 additions and 4 deletions

View File

@ -511,7 +511,7 @@ walk: // Outer loop for walking the tree
*value.params = (*value.params)[:i+1] *value.params = (*value.params)[:i+1]
val := path[:end] val := path[:end]
if unescape { if unescape {
if v, err := url.QueryUnescape(val); err == nil { if v, err := url.PathUnescape(val); err == nil {
val = v val = v
} }
} }
@ -564,7 +564,7 @@ walk: // Outer loop for walking the tree
*value.params = (*value.params)[:i+1] *value.params = (*value.params)[:i+1]
val := path val := path
if unescape { if unescape {
if v, err := url.QueryUnescape(path); err == nil { if v, err := url.PathUnescape(path); err == nil {
val = v val = v
} }
} }

View File

@ -345,10 +345,11 @@ func TestUnescapeParameters(t *testing.T) {
{"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}}, {"/cmd/test/", false, "/cmd/:tool/", Params{Param{Key: "tool", Value: "test"}}},
{"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}}, {"/cmd/test", true, "", Params{Param{Key: "tool", Value: "test"}}},
{"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}}, {"/src/some/file.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file.png"}}},
{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file test.png"}}}, {"/src/some/file+test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file+test.png"}}},
{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file++++%%%%test.png"}}}, {"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file++++%%%%test.png"}}},
{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file/test.png"}}}, {"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file/test.png"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng in ünìcodé"}}}, {"/src/some/file%2Btest.png", false, "/src/*filepath", Params{Param{Key: "filepath", Value: "/some/file+test.png"}}},
{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{Key: "query", Value: "someth!ng+in+ünìcodé"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}}, {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{Key: "user", Value: "slash/gordon"}}}, {"/info/slash%2Fgordon", false, "/info/:user", Params{Param{Key: "user", Value: "slash/gordon"}}},
{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash/gordon"}, Param{Key: "project", Value: "Project #1"}}}, {"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "slash/gordon"}, Param{Key: "project", Value: "Project #1"}}},