Repository: gin-gonic/gin
Pull request #2159 (2019-11-27 13:46:22)

chore(performance): Improve performance for adding RemoveExtraSlash flag
See the detailed benchmark: https://github.com/gin-gonic/gin/pull/2153

before:

```
#GithubAPI Routes: 203
   Gin: 58512 Bytes

#GPlusAPI Routes: 13
   Gin: 4384 Bytes

#ParseAPI Routes: 26
   Gin: 7776 Bytes

#Static Routes: 157
   Gin: 34936 Bytes

goos: darwin
goarch: amd64
pkg: github.com/julienschmidt/go-http-routing-benchmark
BenchmarkGin_Param        	11682692	       107 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_Param5       	 5853684	       205 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_Param20      	 2497855	       484 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_ParamWrite   	 6599636	       180 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GithubStatic 	 9790374	       125 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GithubParam  	 4965781	       244 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GithubAll    	   24424	     49748 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GPlusStatic  	14304789	        86.4 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GPlusParam   	 6814308	       174 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GPlus2Params 	 4847437	       253 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GPlusAll     	  504351	      2302 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_ParseStatic  	12430803	        99.0 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_ParseParam   	10323856	       119 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_Parse2Params 	 7512644	       159 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_ParseAll     	  305469	      3961 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_StaticAll    	   37492	     30986 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/julienschmidt/go-http-routing-benchmark	23.774s
```

after:

```
#GithubAPI Routes: 203
   Gin: 58512 Bytes

#GPlusAPI Routes: 13
   Gin: 4384 Bytes

#ParseAPI Routes: 26
   Gin: 7776 Bytes

#Static Routes: 157
   Gin: 34936 Bytes

goos: darwin
goarch: amd64
pkg: github.com/julienschmidt/go-http-routing-benchmark
BenchmarkGin_Param        	15443652	        78.9 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_Param5       	 9414763	       127 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_Param20      	 4002259	       298 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_ParamWrite   	 7826979	       151 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GithubStatic 	11584101	       103 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GithubParam  	 7606005	       158 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GithubAll    	   36332	     34051 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GPlusStatic  	12782667	        79.5 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GPlusParam   	12041554	       103 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GPlus2Params 	 9379024	       128 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_GPlusAll     	  813216	      1394 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_ParseStatic  	15016789	        77.1 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_ParseParam   	15004710	        82.2 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_Parse2Params 	11183847	       109 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_ParseAll     	  448926	      2553 ns/op	       0 B/op	       0 allocs/op
BenchmarkGin_StaticAll    	   51507	     23745 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/julienschmidt/go-http-routing-benchmark	22.477s
```

ref issue: #1817 and #1644

cc @thinkerou @dmarkham @QianChenglong

codecov[bot]: # [Codecov](https://codecov.io/gh/gin-gonic/gin/pull/2159?src=pr&el=h1) Report
> Merging [#2159](https://codecov.io/gh/gin-gonic/gin/pull/2159?src=pr&el=desc) into [master](https://codecov.io/gh/gin-gonic/gin/commit/231ff00d1f77d30f8dd97278680d7731e1317d55?src=pr&el=desc) will **increase** coverage by `<.01%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/gin-gonic/gin/pull/2159/graphs/tree.svg?width=650&token=Qpqxhwery8&height=150&src=pr)](https://codecov.io/gh/gin-gonic/gin/pull/2159?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #2159      +/-   ##
==========================================
+ Coverage   98.61%   98.61%   +<.01%     
==========================================
  Files          40       40              
  Lines        2238     2240       +2     
==========================================
+ Hits         2207     2209       +2     
  Misses         16       16              
  Partials       15       15
```


| [Impacted Files](https://codecov.io/gh/gin-gonic/gin/pull/2159?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [gin.go](https://codecov.io/gh/gin-gonic/gin/pull/2159/diff?src=pr&el=tree#diff-Z2luLmdv) | `98.29% <100%> (+0.01%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/gin-gonic/gin/pull/2159?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/gin-gonic/gin/pull/2159?src=pr&el=footer). Last update [231ff00...0b58c63](https://codecov.io/gh/gin-gonic/gin/pull/2159?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/gin.go b/gin.go
index c194d6b..f0cd09e 100644
--- a/gin.go
+++ b/gin.go
@@ -97,6 +97,10 @@ type Engine struct {
 	// method call.
 	MaxMultipartMemory int64
 
+	// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
+	// See the PR #1817 and issue #1644
+	RemoveExtraSlash bool
+
 	delims           render.Delims
 	secureJsonPrefix string
 	HTMLRender       render.HTMLRender
@@ -134,6 +138,7 @@ func New() *Engine {
 		ForwardedByClientIP:    true,
 		AppEngine:              defaultAppEngine,
 		UseRawPath:             false,
+		RemoveExtraSlash:       false,
 		UnescapePathValues:     true,
 		MaxMultipartMemory:     defaultMultipartMemory,
 		trees:                  make(methodTrees, 0, 9),
@@ -385,7 +390,10 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
 		rPath = c.Request.URL.RawPath
 		unescape = engine.UnescapePathValues
 	}
-	rPath = cleanPath(rPath)
+
+	if engine.RemoveExtraSlash {
+		rPath = cleanPath(rPath)
+	}
 
 	// Find root of the tree for the given HTTP method
 	t := engine.trees


Test patch:
diff --git a/routes_test.go b/routes_test.go
index 0c2f9a0..d1ddbe9 100644
--- a/routes_test.go
+++ b/routes_test.go
@@ -263,6 +263,7 @@ func TestRouteParamsByNameWithExtraSlash(t *testing.T) {
 	lastName := ""
 	wild := ""
 	router := New()
+	router.RemoveExtraSlash = true
 	router.GET("/test/:name/:last_name/*wild", func(c *Context) {
 		name = c.Params.ByName("name")
 		lastName = c.Params.ByName("last_name")
@@ -407,6 +408,29 @@ func TestRouteNotAllowedDisabled(t *testing.T) {
 	assert.Equal(t, http.StatusNotFound, w.Code)
 }
 
+func TestRouterNotFoundWithRemoveExtraSlash(t *testing.T) {
+	router := New()
+	router.RemoveExtraSlash = true
+	router.GET("/path", func(c *Context) {})
+	router.GET("/", func(c *Context) {})
+
+	testRoutes := []struct {
+		route    string
+		code     int
+		location string
+	}{
+		{"/../path", http.StatusOK, ""},    // CleanPath
+		{"/nope", http.StatusNotFound, ""}, // NotFound
+	}
+	for _, tr := range testRoutes {
+		w := performRequest(router, "GET", tr.route)
+		assert.Equal(t, tr.code, w.Code)
+		if w.Code != http.StatusNotFound {
+			assert.Equal(t, tr.location, fmt.Sprint(w.Header().Get("Location")))
+		}
+	}
+}
+
 func TestRouterNotFound(t *testing.T) {
 	router := New()
 	router.RedirectFixedPath = true
@@ -419,14 +443,14 @@ func TestRouterNotFound(t *testing.T) {
 		code     int
 		location string
 	}{
-		{"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/
-		{"/dir", http.StatusMovedPermanently, "/dir/"},   // TSR +/
-		{"/PATH", http.StatusMovedPermanently, "/path"},  // Fixed Case
-		{"/DIR/", http.StatusMovedPermanently, "/dir/"},  // Fixed Case
-		{"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/
-		{"/DIR", http.StatusMovedPermanently, "/dir/"},   // Fixed Case +/
-		{"/../path", http.StatusOK, ""},                  // CleanPath
-		{"/nope", http.StatusNotFound, ""},               // NotFound
+		{"/path/", http.StatusMovedPermanently, "/path"},   // TSR -/
+		{"/dir", http.StatusMovedPermanently, "/dir/"},     // TSR +/
+		{"/PATH", http.StatusMovedPermanently, "/path"},    // Fixed Case
+		{"/DIR/", http.StatusMovedPermanently, "/dir/"},    // Fixed Case
+		{"/PATH/", http.StatusMovedPermanently, "/path"},   // Fixed Case -/
+		{"/DIR", http.StatusMovedPermanently, "/dir/"},     // Fixed Case +/
+		{"/../path", http.StatusMovedPermanently, "/path"}, // Without CleanPath
+		{"/nope", http.StatusNotFound, ""},                 // NotFound
 	}
 	for _, tr := range testRoutes {
 		w := performRequest(router, "GET", tr.route)