mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-14 20:22:20 +08:00
feat: add an option to allow trailing slash insensitive matching
Signed-off-by: Charlie Chiang <charlie_c_0129@outlook.com>
This commit is contained in:
parent
a4ac275e07
commit
c9ab7eb86f
80
gin.go
80
gin.go
@ -112,6 +112,15 @@ type Engine struct {
|
|||||||
// RedirectTrailingSlash is independent of this option.
|
// RedirectTrailingSlash is independent of this option.
|
||||||
RedirectFixedPath bool
|
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
|
// HandleMethodNotAllowed if enabled, the router checks if another method is allowed for the
|
||||||
// current route, if the current request can not be routed.
|
// current route, if the current request can not be routed.
|
||||||
// If this is the case, the request is answered with 'Method Not Allowed'
|
// 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.
|
// New returns a new blank Engine instance without any middleware attached.
|
||||||
// By default, the configuration is:
|
// By default, the configuration is:
|
||||||
// - RedirectTrailingSlash: true
|
// - RedirectTrailingSlash: true
|
||||||
// - RedirectFixedPath: false
|
// - RedirectFixedPath: false
|
||||||
// - HandleMethodNotAllowed: false
|
// - TrailingSlashInsensitivity: false
|
||||||
// - ForwardedByClientIP: true
|
// - HandleMethodNotAllowed: false
|
||||||
// - UseRawPath: false
|
// - ForwardedByClientIP: true
|
||||||
// - UnescapePathValues: true
|
// - UseRawPath: false
|
||||||
|
// - UnescapePathValues: true
|
||||||
func New(opts ...OptionFunc) *Engine {
|
func New(opts ...OptionFunc) *Engine {
|
||||||
debugPrintWARNINGNew()
|
debugPrintWARNINGNew()
|
||||||
engine := &Engine{
|
engine := &Engine{
|
||||||
@ -198,22 +208,23 @@ func New(opts ...OptionFunc) *Engine {
|
|||||||
basePath: "/",
|
basePath: "/",
|
||||||
root: true,
|
root: true,
|
||||||
},
|
},
|
||||||
FuncMap: template.FuncMap{},
|
FuncMap: template.FuncMap{},
|
||||||
RedirectTrailingSlash: true,
|
RedirectTrailingSlash: true,
|
||||||
RedirectFixedPath: false,
|
RedirectFixedPath: false,
|
||||||
HandleMethodNotAllowed: false,
|
TrailingSlashInsensitivity: false,
|
||||||
ForwardedByClientIP: true,
|
HandleMethodNotAllowed: false,
|
||||||
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
|
ForwardedByClientIP: true,
|
||||||
TrustedPlatform: defaultPlatform,
|
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
|
||||||
UseRawPath: false,
|
TrustedPlatform: defaultPlatform,
|
||||||
RemoveExtraSlash: false,
|
UseRawPath: false,
|
||||||
UnescapePathValues: true,
|
RemoveExtraSlash: false,
|
||||||
MaxMultipartMemory: defaultMultipartMemory,
|
UnescapePathValues: true,
|
||||||
trees: make(methodTrees, 0, 9),
|
MaxMultipartMemory: defaultMultipartMemory,
|
||||||
delims: render.Delims{Left: "{{", Right: "}}"},
|
trees: make(methodTrees, 0, 9),
|
||||||
secureJSONPrefix: "while(1);",
|
delims: render.Delims{Left: "{{", Right: "}}"},
|
||||||
trustedProxies: []string{"0.0.0.0/0", "::/0"},
|
secureJSONPrefix: "while(1);",
|
||||||
trustedCIDRs: defaultTrustedCIDRs,
|
trustedProxies: []string{"0.0.0.0/0", "::/0"},
|
||||||
|
trustedCIDRs: defaultTrustedCIDRs,
|
||||||
}
|
}
|
||||||
engine.engine = engine
|
engine.engine = engine
|
||||||
engine.pool.New = func() any {
|
engine.pool.New = func() any {
|
||||||
@ -691,6 +702,19 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if httpMethod != http.MethodConnect && rPath != "/" {
|
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 {
|
if value.tsr && engine.RedirectTrailingSlash {
|
||||||
redirectTrailingSlash(c)
|
redirectTrailingSlash(c)
|
||||||
return
|
return
|
||||||
@ -745,6 +769,13 @@ func serveError(c *Context, code int, defaultMessage []byte) {
|
|||||||
c.writermem.WriteHeaderNow()
|
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) {
|
func redirectTrailingSlash(c *Context) {
|
||||||
req := c.Request
|
req := c.Request
|
||||||
p := req.URL.Path
|
p := req.URL.Path
|
||||||
@ -754,10 +785,7 @@ func redirectTrailingSlash(c *Context) {
|
|||||||
|
|
||||||
p = prefix + "/" + req.URL.Path
|
p = prefix + "/" + req.URL.Path
|
||||||
}
|
}
|
||||||
req.URL.Path = p + "/"
|
req.URL.Path = addOrRemoveTrailingSlash(p)
|
||||||
if length := len(p); length > 1 && p[length-1] == '/' {
|
|
||||||
req.URL.Path = p[:length-1]
|
|
||||||
}
|
|
||||||
redirectRequest(c)
|
redirectRequest(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
101
routes_test.go
101
routes_test.go
@ -246,6 +246,107 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
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) {
|
func TestRouteRedirectFixedPath(t *testing.T) {
|
||||||
router := New()
|
router := New()
|
||||||
router.RedirectFixedPath = true
|
router.RedirectFixedPath = true
|
||||||
|
Loading…
x
Reference in New Issue
Block a user