Merge c9ab7eb86ff747a0552bef1af4376f7e0405bc38 into c3d1092b3b48addf6f9cd00fe274ec3bd14650eb

This commit is contained in:
Charlie Chiang 2025-10-12 15:10:27 +07:00 committed by GitHub
commit 12a1b6ebe9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 155 additions and 26 deletions

80
gin.go
View File

@ -112,6 +112,15 @@ type Engine struct {
// RedirectTrailingSlash is independent of this option.
RedirectFixedPath bool
// TrailingSlashInsensitivity makes the router insensitive to trailing
// slashes. It works like RedirectTrailingSlash, but instead of generating a
// redirection response to the path with or without the trailing slash, it
// will just go to the corresponding handler.
//
// Enabling this option will make RedirectTrailingSlash ineffective since
// no redirection will be performed.
TrailingSlashInsensitivity bool
// HandleMethodNotAllowed if enabled, the router checks if another method is allowed for the
// current route, if the current request can not be routed.
// If this is the case, the request is answered with 'Method Not Allowed'
@ -184,12 +193,13 @@ var _ IRouter = (*Engine)(nil)
// New returns a new blank Engine instance without any middleware attached.
// By default, the configuration is:
// - RedirectTrailingSlash: true
// - RedirectFixedPath: false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true
// - UseRawPath: false
// - UnescapePathValues: true
// - RedirectTrailingSlash: true
// - RedirectFixedPath: false
// - TrailingSlashInsensitivity: false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true
// - UseRawPath: false
// - UnescapePathValues: true
func New(opts ...OptionFunc) *Engine {
debugPrintWARNINGNew()
engine := &Engine{
@ -198,22 +208,23 @@ func New(opts ...OptionFunc) *Engine {
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJSONPrefix: "while(1);",
trustedProxies: []string{"0.0.0.0/0", "::/0"},
trustedCIDRs: defaultTrustedCIDRs,
FuncMap: template.FuncMap{},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
TrailingSlashInsensitivity: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
TrustedPlatform: defaultPlatform,
UseRawPath: false,
RemoveExtraSlash: false,
UnescapePathValues: true,
MaxMultipartMemory: defaultMultipartMemory,
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJSONPrefix: "while(1);",
trustedProxies: []string{"0.0.0.0/0", "::/0"},
trustedCIDRs: defaultTrustedCIDRs,
}
engine.engine = engine
engine.pool.New = func() any {
@ -691,6 +702,19 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
return
}
if httpMethod != http.MethodConnect && rPath != "/" {
// TrailingSlashInsensitivity has precedence over RedirectTrailingSlash.
if value.tsr && engine.TrailingSlashInsensitivity {
// Retry with the path with or without the trailing slash.
// It should succeed because tsr is true.
value = root.getValue(addOrRemoveTrailingSlash(rPath), c.params, c.skippedNodes, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
}
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
@ -745,6 +769,13 @@ func serveError(c *Context, code int, defaultMessage []byte) {
c.writermem.WriteHeaderNow()
}
func addOrRemoveTrailingSlash(p string) string {
if length := len(p); length > 1 && p[length-1] == '/' {
return p[:length-1]
}
return p + "/"
}
func redirectTrailingSlash(c *Context) {
req := c.Request
p := req.URL.Path
@ -754,10 +785,7 @@ func redirectTrailingSlash(c *Context) {
p = prefix + "/" + req.URL.Path
}
req.URL.Path = p + "/"
if length := len(p); length > 1 && p[length-1] == '/' {
req.URL.Path = p[:length-1]
}
req.URL.Path = addOrRemoveTrailingSlash(p)
redirectRequest(c)
}

View File

@ -246,6 +246,107 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestRouteTrailingSlashInsensitivity(t *testing.T) {
router := New()
router.RedirectTrailingSlash = false
router.TrailingSlashInsensitivity = true
router.GET("/path", func(c *Context) { c.String(http.StatusOK, "path") })
router.GET("/path2/", func(c *Context) { c.String(http.StatusOK, "path2") })
// Test that trailing slash insensitivity works.
w := PerformRequest(router, http.MethodGet, "/path/")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "path", w.Body.String())
w = PerformRequest(router, http.MethodGet, "/path")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "path", w.Body.String())
w = PerformRequest(router, http.MethodGet, "/path2/")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "path2", w.Body.String())
w = PerformRequest(router, http.MethodGet, "/path2")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "path2", w.Body.String())
// If handlers for `/path` and `/path/` are different, the request should not be redirected.
router.GET("/path3", func(c *Context) { c.String(http.StatusOK, "path3") })
router.GET("/path3/", func(c *Context) { c.String(http.StatusOK, "path3/") })
w = PerformRequest(router, http.MethodGet, "/path3")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "path3", w.Body.String())
w = PerformRequest(router, http.MethodGet, "/path3/")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "path3/", w.Body.String())
// Should no longer match.
router.TrailingSlashInsensitivity = false
w = PerformRequest(router, http.MethodGet, "/path2")
assert.Equal(t, http.StatusNotFound, w.Code)
w = PerformRequest(router, http.MethodGet, "/path/")
assert.Equal(t, http.StatusNotFound, w.Code)
}
func BenchmarkRouteTrailingSlashInsensitivity(b *testing.B) {
b.Run("Insensitive", func(b *testing.B) {
router := New()
router.RedirectTrailingSlash = false
router.TrailingSlashInsensitivity = true
router.GET("/path", func(c *Context) { c.String(http.StatusOK, "path") })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Cause an insensitive match. Test if the retry logic is causing
// slowdowns.
w := PerformRequest(router, http.MethodGet, "/path/")
if w.Code != http.StatusOK || w.Body.String() != "path" {
b.Fatalf("Expected status %d, got %d", http.StatusOK, w.Code)
}
}
})
b.Run("Exact", func(b *testing.B) {
router := New()
router.RedirectTrailingSlash = false
router.TrailingSlashInsensitivity = false
router.GET("/path", func(c *Context) { c.String(http.StatusOK, "path") })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
w := PerformRequest(router, http.MethodGet, "/path") // Exact match.
if w.Code != http.StatusOK || w.Body.String() != "path" {
b.Fatalf("Expected status %d, got %d", http.StatusOK, w.Code)
}
}
})
b.Run("Redirect", func(b *testing.B) {
router := New()
router.RedirectTrailingSlash = true
router.TrailingSlashInsensitivity = false
router.GET("/path", func(c *Context) { c.String(http.StatusOK, "path") })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
w := PerformRequest(router, http.MethodGet, "/path/") // Redirect.
if w.Code != http.StatusMovedPermanently {
b.Fatalf("Expected status %d, got %d", http.StatusMovedPermanently, w.Code)
}
}
})
}
func TestRouteRedirectFixedPath(t *testing.T) {
router := New()
router.RedirectFixedPath = true