Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 54 additions & 26 deletions gin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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{
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
}

Expand Down
101 changes: 101 additions & 0 deletions routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading