Skip to content

Commit 1c36de8

Browse files
committed
feat: add an option to allow trailing slash insensitive matching
Signed-off-by: Charlie Chiang <[email protected]>
1 parent a4ac275 commit 1c36de8

File tree

2 files changed

+88
-26
lines changed

2 files changed

+88
-26
lines changed

gin.go

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ type Engine struct {
112112
// RedirectTrailingSlash is independent of this option.
113113
RedirectFixedPath bool
114114

115+
// TrailingSlashInsensitivity makes the router insensitive to trailing
116+
// slashes. It works like RedirectTrailingSlash, but instead of generating a
117+
// redirection response to the path with or without the trailing slash, it
118+
// will just go to the corresponding handler.
119+
//
120+
// Enabling this option will make RedirectTrailingSlash ineffective since
121+
// no redirection will be performed.
122+
TrailingSlashInsensitivity bool
123+
115124
// HandleMethodNotAllowed if enabled, the router checks if another method is allowed for the
116125
// current route, if the current request can not be routed.
117126
// If this is the case, the request is answered with 'Method Not Allowed'
@@ -184,12 +193,13 @@ var _ IRouter = (*Engine)(nil)
184193

185194
// New returns a new blank Engine instance without any middleware attached.
186195
// By default, the configuration is:
187-
// - RedirectTrailingSlash: true
188-
// - RedirectFixedPath: false
189-
// - HandleMethodNotAllowed: false
190-
// - ForwardedByClientIP: true
191-
// - UseRawPath: false
192-
// - UnescapePathValues: true
196+
// - RedirectTrailingSlash: true
197+
// - RedirectFixedPath: false
198+
// - TrailingSlashInsensitivity: false
199+
// - HandleMethodNotAllowed: false
200+
// - ForwardedByClientIP: true
201+
// - UseRawPath: false
202+
// - UnescapePathValues: true
193203
func New(opts ...OptionFunc) *Engine {
194204
debugPrintWARNINGNew()
195205
engine := &Engine{
@@ -198,22 +208,23 @@ func New(opts ...OptionFunc) *Engine {
198208
basePath: "/",
199209
root: true,
200210
},
201-
FuncMap: template.FuncMap{},
202-
RedirectTrailingSlash: true,
203-
RedirectFixedPath: false,
204-
HandleMethodNotAllowed: false,
205-
ForwardedByClientIP: true,
206-
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
207-
TrustedPlatform: defaultPlatform,
208-
UseRawPath: false,
209-
RemoveExtraSlash: false,
210-
UnescapePathValues: true,
211-
MaxMultipartMemory: defaultMultipartMemory,
212-
trees: make(methodTrees, 0, 9),
213-
delims: render.Delims{Left: "{{", Right: "}}"},
214-
secureJSONPrefix: "while(1);",
215-
trustedProxies: []string{"0.0.0.0/0", "::/0"},
216-
trustedCIDRs: defaultTrustedCIDRs,
211+
FuncMap: template.FuncMap{},
212+
RedirectTrailingSlash: true,
213+
RedirectFixedPath: false,
214+
TrailingSlashInsensitivity: false,
215+
HandleMethodNotAllowed: false,
216+
ForwardedByClientIP: true,
217+
RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"},
218+
TrustedPlatform: defaultPlatform,
219+
UseRawPath: false,
220+
RemoveExtraSlash: false,
221+
UnescapePathValues: true,
222+
MaxMultipartMemory: defaultMultipartMemory,
223+
trees: make(methodTrees, 0, 9),
224+
delims: render.Delims{Left: "{{", Right: "}}"},
225+
secureJSONPrefix: "while(1);",
226+
trustedProxies: []string{"0.0.0.0/0", "::/0"},
227+
trustedCIDRs: defaultTrustedCIDRs,
217228
}
218229
engine.engine = engine
219230
engine.pool.New = func() any {
@@ -691,6 +702,19 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
691702
return
692703
}
693704
if httpMethod != http.MethodConnect && rPath != "/" {
705+
// TrailingSlashInsensitivity has precedence over RedirectTrailingSlash.
706+
if value.tsr && engine.TrailingSlashInsensitivity {
707+
// Retry with the path with or without the trailing slash.
708+
// It should succeed because tsr is true.
709+
value = root.getValue(addOrRemoveTrailingSlash(rPath), c.params, c.skippedNodes, unescape)
710+
if value.handlers != nil {
711+
c.handlers = value.handlers
712+
c.fullPath = value.fullPath
713+
c.Next()
714+
c.writermem.WriteHeaderNow()
715+
return
716+
}
717+
}
694718
if value.tsr && engine.RedirectTrailingSlash {
695719
redirectTrailingSlash(c)
696720
return
@@ -745,6 +769,13 @@ func serveError(c *Context, code int, defaultMessage []byte) {
745769
c.writermem.WriteHeaderNow()
746770
}
747771

772+
func addOrRemoveTrailingSlash(p string) string {
773+
if strings.HasSuffix(p, "/") {
774+
return p[:len(p)-1]
775+
}
776+
return p + "/"
777+
}
778+
748779
func redirectTrailingSlash(c *Context) {
749780
req := c.Request
750781
p := req.URL.Path
@@ -754,10 +785,7 @@ func redirectTrailingSlash(c *Context) {
754785

755786
p = prefix + "/" + req.URL.Path
756787
}
757-
req.URL.Path = p + "/"
758-
if length := len(p); length > 1 && p[length-1] == '/' {
759-
req.URL.Path = p[:length-1]
760-
}
788+
req.URL.Path = addOrRemoveTrailingSlash(p)
761789
redirectRequest(c)
762790
}
763791

routes_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,40 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
246246
assert.Equal(t, http.StatusNotFound, w.Code)
247247
}
248248

249+
func TestRouteTrailingSlashInsensitivity(t *testing.T) {
250+
router := New()
251+
router.TrailingSlashInsensitivity = true
252+
router.GET("/path", func(c *Context) { c.String(http.StatusOK, "path") })
253+
router.GET("/path2/", func(c *Context) { c.String(http.StatusOK, "path2") })
254+
255+
w := PerformRequest(router, http.MethodGet, "/path/")
256+
assert.Equal(t, http.StatusOK, w.Code)
257+
assert.Equal(t, "path", w.Body.String())
258+
259+
w = PerformRequest(router, http.MethodGet, "/path")
260+
assert.Equal(t, http.StatusOK, w.Code)
261+
assert.Equal(t, "path", w.Body.String())
262+
263+
w = PerformRequest(router, http.MethodGet, "/path2/")
264+
assert.Equal(t, http.StatusOK, w.Code)
265+
assert.Equal(t, "path2", w.Body.String())
266+
267+
w = PerformRequest(router, http.MethodGet, "/path2")
268+
assert.Equal(t, http.StatusOK, w.Code)
269+
assert.Equal(t, "path2", w.Body.String())
270+
271+
// If handlers for `/path` and `/path/` are different, the request should not be redirected.
272+
router.GET("/path/", func(c *Context) { c.String(http.StatusOK, "path/") })
273+
274+
w = PerformRequest(router, http.MethodGet, "/path")
275+
assert.Equal(t, http.StatusOK, w.Code)
276+
assert.Equal(t, "path", w.Body.String())
277+
278+
w = PerformRequest(router, http.MethodGet, "/path/")
279+
assert.Equal(t, http.StatusOK, w.Code)
280+
assert.Equal(t, "path/", w.Body.String())
281+
}
282+
249283
func TestRouteRedirectFixedPath(t *testing.T) {
250284
router := New()
251285
router.RedirectFixedPath = true

0 commit comments

Comments
 (0)