From 09b561866fb1063ddc61fb0d1b55f03f1e2fc781 Mon Sep 17 00:00:00 2001 From: b84330808 Date: Mon, 2 Nov 2026 00:54:58 +0900 Subject: [PATCH 1/2] fix(gin): keep encoded slashes in trailing slash redirects With UseRawPath or UseEscapedPath the router matches the escaped path, but redirectTrailingSlash built the Location from the decoded URL.Path. A request for /files/a%2Fb/ was redirected to /files/a/b, which no longer matches /files/:name. Build the redirect from the escaped path when routing used it, and keep URL.RawPath in sync so the encoding survives. Co-Authored-By: Claude Opus 5.5 --- gin.go | 15 ++++++++++++++- routes_test.go | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/gin.go b/gin.go index a160727597..610122aed2 100644 --- a/gin.go +++ b/gin.go @@ -9,6 +9,7 @@ import ( "html/template" "net" "net/http" + "net/url" "os" "path" "strings" @@ -810,16 +811,28 @@ func serveError(c *Context, code int, defaultMessage []byte) { func redirectTrailingSlash(c *Context) { req := c.Request p := req.URL.Path + // Routing matched the escaped path, so build the redirect from it too; + // otherwise an encoded slash (%2F) would turn into a real path separator. + escaped := c.engine.UseEscapedPath || (c.engine.UseRawPath && req.URL.RawPath != "") + if escaped { + p = req.URL.EscapedPath() + } if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." { prefix = sanitizePathChars(prefix) prefix = removeRepeatedChar(prefix, '/') - p = prefix + "/" + req.URL.Path + p = prefix + "/" + p } req.URL.Path = p + "/" if length := len(p); length > 1 && p[length-1] == '/' { req.URL.Path = p[:length-1] } + if escaped { + if unescaped, err := url.PathUnescape(req.URL.Path); err == nil { + req.URL.RawPath = req.URL.Path + req.URL.Path = unescaped + } + } redirectRequest(c) } diff --git a/routes_test.go b/routes_test.go index a032a2b76a..e20640b9c2 100644 --- a/routes_test.go +++ b/routes_test.go @@ -303,6 +303,24 @@ func TestRouteRedirectTrailingSlash(t *testing.T) { assert.Equal(t, http.StatusNotFound, w.Code) } +func TestRouteRedirectTrailingSlashKeepsEscapedPath(t *testing.T) { + for _, useEscapedPath := range []bool{false, true} { + router := New() + router.UseRawPath = !useEscapedPath + router.UseEscapedPath = useEscapedPath + router.GET("/files/:name", func(c *Context) {}) + router.GET("/dirs/:name/", func(c *Context) {}) + + w := PerformRequest(router, http.MethodGet, "/files/a%2Fb/") + assert.Equal(t, http.StatusMovedPermanently, w.Code) + assert.Equal(t, "/files/a%2Fb", w.Header().Get("Location")) + + w = PerformRequest(router, http.MethodGet, "/dirs/a%2Fb") + assert.Equal(t, http.StatusMovedPermanently, w.Code) + assert.Equal(t, "/dirs/a%2Fb/", w.Header().Get("Location")) + } +} + func TestRouteRedirectFixedPath(t *testing.T) { router := New() router.RedirectFixedPath = true From 5793a439a54ff3119e046b169525f50cac89e988 Mon Sep 17 00:00:00 2001 From: b84330808 Date: Tue, 3 Nov 2026 11:18:31 +0900 Subject: [PATCH 2/2] test(gin): cover X-Forwarded-Prefix in the escaped trailing slash redirect Co-Authored-By: Claude Opus 5.5 --- routes_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/routes_test.go b/routes_test.go index e20640b9c2..8e31a9519f 100644 --- a/routes_test.go +++ b/routes_test.go @@ -318,6 +318,10 @@ func TestRouteRedirectTrailingSlashKeepsEscapedPath(t *testing.T) { w = PerformRequest(router, http.MethodGet, "/dirs/a%2Fb") assert.Equal(t, http.StatusMovedPermanently, w.Code) assert.Equal(t, "/dirs/a%2Fb/", w.Header().Get("Location")) + + w = PerformRequest(router, http.MethodGet, "/files/a%2Fb/", header{Key: "X-Forwarded-Prefix", Value: "/api"}) + assert.Equal(t, http.StatusMovedPermanently, w.Code) + assert.Equal(t, "/api/files/a%2Fb", w.Header().Get("Location")) } }