Go version
go version go1.25.0 darwin/arm64
Output of go env
in your module/workspace:
AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/david/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/david/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/_c/9qfk97x92zq400p82xhtbw240000gn/T/go-build1209874320=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/Users/david/dev/go/harpo/go.mod'
GOMODCACHE='/Users/david/go/1.25.0/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/david/go/1.25.0'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/david/.goenv/versions/1.25.0'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/david/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/Users/david/.goenv/versions/1.25.0/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.25.0'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
I'm implementing an HTTP API that uses a "path query". RFC 3986 describes it, while RFC 6570 more formally defines it. The format is /;key=value;key=value
. In order to work properly, ;
and =
path-encoded in keys or values must not be decoded. Otherwise there is no way to distinguish them from their literal use as query delimiters. This test demonstrates the issue (playground equivalent):
func TestMuxPatternDecoding(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/{query}", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.PathValue("query")))
})
// Pass.
req := httptest.NewRequest(http.MethodGet, "/;x=y", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Body.String() != ";x=y" {
t.Fatalf("Expected `;x=y` but got `%v`", w.Body.String())
}
// Fail.
req = httptest.NewRequest(http.MethodGet, "/;x=y%3ba%3db", nil)
w = httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Body.String() != ";x=y%3ba%3db" {
t.Fatalf("Expected `;x=y%%3ba%%3db` but got `%v`", w.Body.String())
}
}
What did you see happen?
Path patterns saved into Request.PathValue have all path-encoded values decoded as a Go string.
What did you expect to see?
In order to support patterns such as path queries, there needs to be a way to access un-decoded path pattern values. Perhaps follow the precedent of the Path
and RawPath
fields in net/url.URL and provide a Request.RawPathValue method?
Comment From: atdiar
You can use the Path or EscapedPath that is present on the URL object. Note that the leading slash will remain.
https://go.dev/play/p/t0UZhtqMzEk
Basically, this type of queries is not handled by default so you need to get back the path and parse it yourself.
// You can edit this code!
// Click here and start typing.
package main
import (
"log"
"net/http"
"net/http/httptest"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.URL.EscapedPath()))
})
// Pass.
req := httptest.NewRequest(http.MethodGet, "/;x=y", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Body.String() != "/;x=y" {
log.Fatalf("Expected `;x=y` but got `%v`", w.Body.String())
}
// Fail.
req = httptest.NewRequest(http.MethodGet, "/;x=y%3ba%3db", nil)
w = httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Body.String() != "/;x=y%3ba%3db" {
log.Fatalf("Expected `;x=y%%3ba%%3db` but got `%v`", w.Body.String())
}
}
Comment From: atdiar
Hold on wait, I might have misunderstood the issue. You want the un-encoded path and that wouldn't give you that.. hmmh.
I tried with RawPath but I get an empty string.
edit: ok I get it.
You need r.URL.RawPath but the field only gets populated if it is different from r.URL.Path
So the first case will error out if you use r.URL.RawPath because it expects Path. The second case should now pass the test with r.URL.RawPath instead of r.PathValue("query")
That will require a little bit of logic on your part to juggle this behavior I guess.
Comment From: theory
I want the un-encoded segment of the path that matched a pattern. I of course could grab the raw path and parse the whole thing myself (my app has more patterns and path segments than the test example here), but then I'm repeating a lot of the work already done by ServeMux.
I'm currently working around this issue by using chi, which doesn't decode a pattern it matches before storing it in the PathValue
.
Comment From: theory
I tried with RawPath but I get an empty string.
Ah, yeah, if RawPath
isn't set then I can't parse it myself. But I could parse EscapedPath
. Seems redundant, though.
Comment From: gabyhelp
Related Issues
- net/http: ServeMux uses URL.Path instead of URL.EscapedPath(), leading it to treat %2F as a path separator #14815 (closed)
- net/http: Conflicts with pattern when using NewServeMux #68838 (closed)
- net/http: ServeMux.Handler does not populate named path wildcards #69623 (closed)
- net/url: RawPath unset even though Path differs from input #48854 (closed)
- net/http: Pattern matching of ServeMux works weird and unexpected #70311 (closed)
- net/url: url.Values.Encode uses `=` for empty value in key=value #30025 (closed)
- net/http: ServerMux does not sanitize ".%2e" ("..") from url path #70130 (closed)
- net/http: ServeMux behaves inconsistently across operating systems #66288 (closed)
Related Code Changes
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
Comment From: theory
14815 looks like the same issue, and @bradfitz is of course right changing it would break something. Hence my suggestion to add a RawPathValue
method (or maybe EscapedPathValue
?).
Comment From: seankhliao
I think this is a case where the additional expansion in API isn't worth it, and you should parse directly from r.URL
, similar to how we don't preserve the transport encodings for other fields.
If anything, this sounds like it should be using a dedicated mux which understands how to parse and route key-value fields, rather than using a net/http.ServeMux which is based on the typical path hierarchy. Note that net/http.Request has SetPathValue which allows third party muxes to store data in the form it needs.
Comment From: theory
Reasonable to suggest other muxes. SetPathValue
is what chi uses, populating it with an unescaped value, which is just what I need. Parsing the resulting value isn't bad; 18 loc.