Repository: gin-gonic/gin (go)

Issue:
404 when using dynamic routing
- With issues:
  - Use the search tool before opening a new issue.
  - Please provide source code and commit sha if you found a bug.
  - Review existing issues and provide feedback or react to them.

## Description
It will bring 404 problem when using dynamic routing(*,:) like below , by calling http://localhost:8080/all. It seemed can not match the path with prefix 'a', such as 'all'.

<!-- Description of a problem -->

## How to reproduce

<!-- The smallest possible code example to show the problem that can be compiled, like -->
```
package main

import (
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.GET("/aa/*xx", newHandler("/aa/*xx"))
	r.GET("/ab/*xx", newHandler("/ab/*xx"))
	r.GET("/:cc", newHandler("/:cc"))

	r.Run() // listen and serve on 0.0.0.0:8080 
}

func newHandler(t string) func(c *gin.Context) {
	return func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": t,
		})
	}
}
```

## Expectations

<!-- Your expectation result of 'curl' command, like -->
```
$ curl http://localhost:8080/all
/:cc
```

## Actual result

<!-- Actual result showing the problem -->
```
$ curl http://localhost:8080/all
404 page not found
```

## Environment

- go version:
- gin version (or commit ref): 1.7.2
- operating system:

Pull request:
fix #2762
#### content

* handle level 1 router match
* It does not affect the matching of the following routes
* level1 router match Will cause params to fail
```
file: tree.test
line: 198 199
new PR handle
```


Patch:
diff --git a/binding/binding.go b/binding/binding.go
index 5caeb58..deb7166 100644
--- a/binding/binding.go
+++ b/binding/binding.go
@@ -49,7 +49,7 @@ type BindingUri interface {
 // StructValidator is the minimal interface which needs to be implemented in
 // order for it to be used as the validator engine for ensuring the correctness
 // of the request. Gin provides a default implementation for this using
-// https://github.com/go-playground/validator/tree/v8.18.2.
+// https://github.com/go-playground/validator/tree/v10.6.1.
 type StructValidator interface {
 	// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
 	// If the received type is a slice|array, the validation should be performed travel on every element.
@@ -65,7 +65,7 @@ type StructValidator interface {
 }
 
 // Validator is the default validator which implements the StructValidator
-// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
+// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
 // under the hood.
 var Validator StructValidator = &defaultValidator{}
 
diff --git a/binding/binding_nomsgpack.go b/binding/binding_nomsgpack.go
index 9afa3dc..2342447 100644
--- a/binding/binding_nomsgpack.go
+++ b/binding/binding_nomsgpack.go
@@ -47,7 +47,7 @@ type BindingUri interface {
 // StructValidator is the minimal interface which needs to be implemented in
 // order for it to be used as the validator engine for ensuring the correctness
 // of the request. Gin provides a default implementation for this using
-// https://github.com/go-playground/validator/tree/v8.18.2.
+// https://github.com/go-playground/validator/tree/v10.6.1.
 type StructValidator interface {
 	// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
 	// If the received type is not a struct, any validation should be skipped and nil must be returned.
@@ -62,7 +62,7 @@ type StructValidator interface {
 }
 
 // Validator is the default validator which implements the StructValidator
-// interface. It uses https://github.com/go-playground/validator/tree/v8.18.2
+// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
 // under the hood.
 var Validator StructValidator = &defaultValidator{}
 
diff --git a/context.go b/context.go
index ede16fd..ecf74ba 100644
--- a/context.go
+++ b/context.go
@@ -40,7 +40,8 @@ const (
 // BodyBytesKey indicates a default body bytes key.
 const BodyBytesKey = "_gin-gonic/gin/bodybyteskey"
 
-const abortIndex int8 = math.MaxInt8 / 2
+// abortIndex represents a typical value used in abort functions.
+const abortIndex int8 = math.MaxInt8 >> 1
 
 // Context is the most important part of gin. It allows us to pass variables between middleware,
 // manage the flow, validate the JSON of a request and render a JSON response for example.
@@ -732,7 +733,7 @@ func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (e
 // If the headers are nots syntactically valid OR the remote IP does not correspong to a trusted proxy,
 // the remote IP (coming form Request.RemoteAddr) is returned.
 func (c *Context) ClientIP() string {
-	// Check if we're running on a tursted platform
+	// Check if we're running on a trusted platform
 	switch c.engine.TrustedPlatform {
 	case PlatformGoogleAppEngine:
 		if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
diff --git a/gin.go b/gin.go
index fd4fa51..6ab2be6 100644
--- a/gin.go
+++ b/gin.go
@@ -99,6 +99,23 @@ type Engine struct {
 	// `(*gin.Context).Request.RemoteAddr`.
 	ForwardedByClientIP bool
 
+	// DEPRECATED: USE `TrustedPlatform` WITH VALUE `gin.GoogleAppEngine` INSTEAD
+	// #726 #755 If enabled, it will trust some headers starting with
+	// 'X-AppEngine...' for better integration with that PaaS.
+	AppEngine bool
+
+	// If enabled, the url.RawPath will be used to find parameters.
+	UseRawPath bool
+
+	// If true, the path value will be unescaped.
+	// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
+	// as url.Path gonna be used, which is already unescaped.
+	UnescapePathValues bool
+
+	// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
+	// See the PR #1817 and issue #1644
+	RemoveExtraSlash bool
+
 	// List of headers used to obtain the client IP when
 	// `(*gin.Engine).ForwardedByClientIP` is `true` and
 	// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
@@ -115,27 +132,10 @@ type Engine struct {
 	// that platform, for example to determine the client IP
 	TrustedPlatform string
 
-	// DEPRECATED: USE `TrustedPlatform` WITH VALUE `gin.GoogleAppEngine` INSTEAD
-	// #726 #755 If enabled, it will trust some headers starting with
-	// 'X-AppEngine...' for better integration with that PaaS.
-	AppEngine bool
-
-	// If enabled, the url.RawPath will be used to find parameters.
-	UseRawPath bool
-
-	// If true, the path value will be unescaped.
-	// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
-	// as url.Path gonna be used, which is already unescaped.
-	UnescapePathValues bool
-
 	// Value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
 	// 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
@@ -539,7 +539,7 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
 			c.writermem.WriteHeaderNow()
 			return
 		}
-		if httpMethod != "CONNECT" && rPath != "/" {
+		if httpMethod != http.MethodConnect && rPath != "/" {
 			if value.tsr && engine.RedirectTrailingSlash {
 				redirectTrailingSlash(c)
 				return
diff --git a/routergroup.go b/routergroup.go
index 6f14bf5..bb24bd5 100644
--- a/routergroup.go
+++ b/routergroup.go
@@ -214,9 +214,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
 
 func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
 	finalSize := len(group.Handlers) + len(handlers)
-	if finalSize >= int(abortIndex) {
-		panic("too many handlers")
-	}
+	assert1(finalSize < int(abortIndex), "too many handlers")
 	mergedHandlers := make(HandlersChain, finalSize)
 	copy(mergedHandlers, group.Handlers)
 	copy(mergedHandlers[len(group.Handlers):], handlers)
diff --git a/tree.go b/tree.go
index 4b5656f..2e46b8e 100644
--- a/tree.go
+++ b/tree.go
@@ -118,11 +118,6 @@ type node struct {
 	fullPath  string
 }
 
-type skip struct {
-	path      string
-	paramNode *node
-}
-
 // Increments priority of the given child and reorders if necessary
 func (n *node) incrementChildPrio(pos int) int {
 	cs := n.children
@@ -405,7 +400,23 @@ type nodeValue struct {
 // made if a handle exists with an extra (without the) trailing slash for the
 // given path.
 func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) {
-	var skipped *skip
+	// path: /abc/123/def
+	// level 1 router:abc
+	// level 2 router:123
+	// level 3 router:def
+	var (
+		skippedPath string
+		latestNode  = n // not found `level 2 router` use latestNode
+
+		// match '/' count
+		// matchNum < 1: `level 2 router` not found,the current node needs to be equal to latestNode
+		// matchNum >= 1: `level (2 or 3 or 4 or ...) router`: Normal handling
+		matchNum int // each match will accumulate
+	)
+	//if path == "/", no need to look for tree node
+	if len(path) == 1 {
+		matchNum = 1
+	}
 
 walk: // Outer loop for walking the tree
 	for {
@@ -418,32 +429,41 @@ walk: // Outer loop for walking the tree
 				idxc := path[0]
 				for i, c := range []byte(n.indices) {
 					if c == idxc {
-						if strings.HasPrefix(n.children[len(n.children)-1].path, ":") {
-							skipped = &skip{
-								path: prefix + path,
-								paramNode: &node{
-									path:      n.path,
-									wildChild: n.wildChild,
-									nType:     n.nType,
-									priority:  n.priority,
-									children:  n.children,
-									handlers:  n.handlers,
-									fullPath:  n.fullPath,
-								},
+						//  strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild
+						if n.wildChild {
+							skippedPath = prefix + path
+							latestNode = &node{
+								path:      n.path,
+								wildChild: n.wildChild,
+								nType:     n.nType,
+								priority:  n.priority,
+								children:  n.children,
+								handlers:  n.handlers,
+								fullPath:  n.fullPath,
 							}
 						}
 
 						n = n.children[i]
+
+						// match '/', If this condition is matched, the next route is found
+						if (len(n.fullPath) != 0 && n.wildChild) || path[len(path)-1] == '/' {
+							matchNum++
+						}
 						continue walk
 					}
 				}
 
+				// level 2 router not found,the current node needs to be equal to latestNode
+				if matchNum < 1 {
+					n = latestNode
+				}
+
 				// If there is no wildcard pattern, recommend a redirection
 				if !n.wildChild {
 					// Nothing found.
 					// We can recommend to redirect to the same URL without a
 					// trailing slash if a leaf exists for that path.
-					value.tsr = (path == "/" && n.handlers != nil)
+					value.tsr = path == "/" && n.handlers != nil
 					return
 				}
 
@@ -483,6 +503,18 @@ walk: // Outer loop for walking the tree
 						if len(n.children) > 0 {
 							path = path[end:]
 							n = n.children[0]
+							// next node,the latestNode needs to be equal to currentNode and handle next router
+							latestNode = n
+							// not found router in (level 1 router and handle next node),skippedPath cannot execute
+							// example:
+							// * /:cc/cc
+							// call /a/cc 	     expectations:match/200      Actual:match/200
+							// call /a/dd 	     expectations:unmatch/404    Actual: panic
+							// call /addr/dd/aa  expectations:unmatch/404    Actual: panic
+							// skippedPath: It can only be executed if the secondary route is not found
+							// matchNum: Go to the next level of routing tree node search,need add matchNum
+							skippedPath = ""
+							matchNum++
 							continue walk
 						}
 
@@ -535,6 +567,10 @@ walk: // Outer loop for walking the tree
 		}
 
 		if path == prefix {
+			// level 2 router not found and latestNode.wildChild is true
+			if matchNum < 1 && latestNode.wildChild {
+				n = latestNode.children[len(latestNode.children)-1]
+			}
 			// We should have reached the node containing the handle.
 			// Check if this node has a handle registered.
 			if value.handlers = n.handlers; value.handlers != nil {
@@ -564,18 +600,18 @@ walk: // Outer loop for walking the tree
 			return
 		}
 
-		if path != "/" && skipped != nil && strings.HasSuffix(skipped.path, path) {
-			path = skipped.path
-			n = skipped.paramNode
-			skipped = nil
+		// path != "/" && skippedPath != ""
+		if len(path) != 1 && len(skippedPath) > 0 && strings.HasSuffix(skippedPath, path) {
+			path = skippedPath
+			n = latestNode
+			skippedPath = ""
 			continue walk
 		}
 
 		// Nothing found. We can recommend to redirect to the same URL with an
 		// extra trailing slash if a leaf exists for that path
-		value.tsr = (path == "/") ||
-			(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
-				path == prefix[:len(prefix)-1] && n.handlers != nil)
+		value.tsr = path == "/" ||
+			(len(prefix) == len(path)+1 && n.handlers != nil)
 		return
 	}
 }


Test patch:
diff --git a/gin_integration_test.go b/gin_integration_test.go
index 2eb2d52..ed7196f 100644
--- a/gin_integration_test.go
+++ b/gin_integration_test.go
@@ -22,7 +22,15 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func testRequest(t *testing.T, url string) {
+// params[0]=url example:http://127.0.0.1:8080/index (cannot be empty)
+// params[1]=response status (custom compare status) default:"200 OK"
+// params[2]=response body (custom compare content)  default:"it worked"
+func testRequest(t *testing.T, params ...string) {
+
+	if len(params) == 0 {
+		t.Fatal("url cannot be empty")
+	}
+
 	tr := &http.Transport{
 		TLSClientConfig: &tls.Config{
 			InsecureSkipVerify: true,
@@ -30,14 +38,27 @@ func testRequest(t *testing.T, url string) {
 	}
 	client := &http.Client{Transport: tr}
 
-	resp, err := client.Get(url)
+	resp, err := client.Get(params[0])
 	assert.NoError(t, err)
 	defer resp.Body.Close()
 
 	body, ioerr := ioutil.ReadAll(resp.Body)
 	assert.NoError(t, ioerr)
-	assert.Equal(t, "it worked", string(body), "resp body should match")
-	assert.Equal(t, "200 OK", resp.Status, "should get a 200")
+
+	var responseStatus = "200 OK"
+	if len(params) > 1 && params[1] != "" {
+		responseStatus = params[1]
+	}
+
+	var responseBody = "it worked"
+	if len(params) > 2 && params[2] != "" {
+		responseBody = params[2]
+	}
+
+	assert.Equal(t, responseStatus, resp.Status, "should get a "+responseStatus)
+	if responseStatus == "200 OK" {
+		assert.Equal(t, responseBody, string(body), "resp body should match")
+	}
 }
 
 func TestRunEmpty(t *testing.T) {
@@ -373,3 +394,52 @@ func testGetRequestHandler(t *testing.T, h http.Handler, url string) {
 	assert.Equal(t, "it worked", w.Body.String(), "resp body should match")
 	assert.Equal(t, 200, w.Code, "should get a 200")
 }
+
+func TestTreeRunDynamicRouting(t *testing.T) {
+	router := New()
+	router.GET("/aa/*xx", func(c *Context) { c.String(http.StatusOK, "/aa/*xx") })
+	router.GET("/ab/*xx", func(c *Context) { c.String(http.StatusOK, "/ab/*xx") })
+	router.GET("/", func(c *Context) { c.String(http.StatusOK, "home") })
+	router.GET("/:cc", func(c *Context) { c.String(http.StatusOK, "/:cc") })
+	router.GET("/:cc/cc", func(c *Context) { c.String(http.StatusOK, "/:cc/cc") })
+	router.GET("/:cc/:dd/ee", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/ee") })
+	router.GET("/:cc/:dd/:ee/ff", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/ff") })
+	router.GET("/:cc/:dd/:ee/:ff/gg", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/gg") })
+	router.GET("/:cc/:dd/:ee/:ff/:gg/hh", func(c *Context) { c.String(http.StatusOK, "/:cc/:dd/:ee/:ff/:gg/hh") })
+	router.GET("/get/test/abc/", func(c *Context) { c.String(http.StatusOK, "/get/test/abc/") })
+	router.GET("/get/:param/abc/", func(c *Context) { c.String(http.StatusOK, "/get/:param/abc/") })
+	router.GET("/something/:paramname/thirdthing", func(c *Context) { c.String(http.StatusOK, "/something/:paramname/thirdthing") })
+	router.GET("/something/secondthing/test", func(c *Context) { c.String(http.StatusOK, "/something/secondthing/test") })
+
+	ts := httptest.NewServer(router)
+	defer ts.Close()
+
+	testRequest(t, ts.URL+"/", "", "home")
+	testRequest(t, ts.URL+"/aa/aa", "", "/aa/*xx")
+	testRequest(t, ts.URL+"/ab/ab", "", "/ab/*xx")
+	testRequest(t, ts.URL+"/all", "", "/:cc")
+	testRequest(t, ts.URL+"/all/cc", "", "/:cc/cc")
+	testRequest(t, ts.URL+"/a/cc", "", "/:cc/cc")
+	testRequest(t, ts.URL+"/c/d/ee", "", "/:cc/:dd/ee")
+	testRequest(t, ts.URL+"/c/d/e/ff", "", "/:cc/:dd/:ee/ff")
+	testRequest(t, ts.URL+"/c/d/e/f/gg", "", "/:cc/:dd/:ee/:ff/gg")
+	testRequest(t, ts.URL+"/c/d/e/f/g/hh", "", "/:cc/:dd/:ee/:ff/:gg/hh")
+	testRequest(t, ts.URL+"/a", "", "/:cc")
+	testRequest(t, ts.URL+"/get/test/abc/", "", "/get/test/abc/")
+	testRequest(t, ts.URL+"/get/te/abc/", "", "/get/:param/abc/")
+	testRequest(t, ts.URL+"/get/xx/abc/", "", "/get/:param/abc/")
+	testRequest(t, ts.URL+"/get/tt/abc/", "", "/get/:param/abc/")
+	testRequest(t, ts.URL+"/get/a/abc/", "", "/get/:param/abc/")
+	testRequest(t, ts.URL+"/get/t/abc/", "", "/get/:param/abc/")
+	testRequest(t, ts.URL+"/get/aa/abc/", "", "/get/:param/abc/")
+	testRequest(t, ts.URL+"/get/abas/abc/", "", "/get/:param/abc/")
+	testRequest(t, ts.URL+"/something/secondthing/test", "", "/something/secondthing/test")
+	testRequest(t, ts.URL+"/something/abcdad/thirdthing", "", "/something/:paramname/thirdthing")
+	testRequest(t, ts.URL+"/something/se/thirdthing", "", "/something/:paramname/thirdthing")
+	testRequest(t, ts.URL+"/something/s/thirdthing", "", "/something/:paramname/thirdthing")
+	testRequest(t, ts.URL+"/something/secondthing/thirdthing", "", "/something/:paramname/thirdthing")
+	// 404 not found
+	testRequest(t, ts.URL+"/a/dd", "404 Not Found")
+	testRequest(t, ts.URL+"/addr/dd/aa", "404 Not Found")
+	testRequest(t, ts.URL+"/something/secondthing/121", "404 Not Found")
+}
diff --git a/routergroup_test.go b/routergroup_test.go
index 0e49d65..d6d8b45 100644
--- a/routergroup_test.go
+++ b/routergroup_test.go
@@ -112,15 +112,19 @@ func TestRouterGroupInvalidStaticFile(t *testing.T) {
 }
 
 func TestRouterGroupTooManyHandlers(t *testing.T) {
+	const (
+		panicValue = "too many handlers"
+		maximumCnt = abortIndex
+	)
 	router := New()
-	handlers1 := make([]HandlerFunc, 40)
+	handlers1 := make([]HandlerFunc, maximumCnt-1)
 	router.Use(handlers1...)
 
-	handlers2 := make([]HandlerFunc, 26)
-	assert.Panics(t, func() {
+	handlers2 := make([]HandlerFunc, maximumCnt+1)
+	assert.PanicsWithValue(t, panicValue, func() {
 		router.Use(handlers2...)
 	})
-	assert.Panics(t, func() {
+	assert.PanicsWithValue(t, panicValue, func() {
 		router.GET("/", handlers2...)
 	})
 }
diff --git a/tree_test.go b/tree_test.go
index 7459317..91213ee 100644
--- a/tree_test.go
+++ b/tree_test.go
@@ -154,6 +154,18 @@ func TestTreeWildcard(t *testing.T) {
 		"/info/:user/public",
 		"/info/:user/project/:project",
 		"/info/:user/project/golang",
+		"/aa/*xx",
+		"/ab/*xx",
+		"/:cc",
+		"/:cc/cc",
+		"/:cc/:dd/ee",
+		"/:cc/:dd/:ee/ff",
+		"/:cc/:dd/:ee/:ff/gg",
+		"/:cc/:dd/:ee/:ff/:gg/hh",
+		"/get/test/abc/",
+		"/get/:param/abc/",
+		"/something/:paramname/thirdthing",
+		"/something/secondthing/test",
 	}
 	for _, route := range routes {
 		tree.addRoute(route, fakeHandler(route))
@@ -186,6 +198,30 @@ func TestTreeWildcard(t *testing.T) {
 		{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
 		{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
 		{"/info/gordon/project/golang", false, "/info/:user/project/golang", Params{Param{Key: "user", Value: "gordon"}}},
+		{"/aa/aa", false, "/aa/*xx", Params{Param{Key: "xx", Value: "/aa"}}},
+		{"/ab/ab", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/ab"}}},
+		{"/a", false, "/:cc", Params{Param{Key: "cc", Value: "a"}}},
+		// * level 1 router match param will be Intercept first
+		// new PR handle (/all /all/cc /a/cc)
+		{"/all", false, "/:cc", Params{Param{Key: "cc", Value: "ll"}}},
+		{"/all/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: "ll"}}},
+		{"/a/cc", false, "/:cc/cc", Params{Param{Key: "cc", Value: ""}}},
+		{"/get/test/abc/", false, "/get/test/abc/", nil},
+		{"/get/te/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "te"}}},
+		{"/get/xx/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "xx"}}},
+		{"/get/tt/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "tt"}}},
+		{"/get/a/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "a"}}},
+		{"/get/t/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "t"}}},
+		{"/get/aa/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "aa"}}},
+		{"/get/abas/abc/", false, "/get/:param/abc/", Params{Param{Key: "param", Value: "abas"}}},
+		{"/something/secondthing/test", false, "/something/secondthing/test", nil},
+		{"/something/abcdad/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "abcdad"}}},
+		{"/something/se/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "se"}}},
+		{"/something/s/thirdthing", false, "/something/:paramname/thirdthing", Params{Param{Key: "paramname", Value: "s"}}},
+		{"/c/d/ee", false, "/:cc/:dd/ee", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}}},
+		{"/c/d/e/ff", false, "/:cc/:dd/:ee/ff", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}}},
+		{"/c/d/e/f/gg", false, "/:cc/:dd/:ee/:ff/gg", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}}},
+		{"/c/d/e/f/g/hh", false, "/:cc/:dd/:ee/:ff/:gg/hh", Params{Param{Key: "cc", Value: "c"}, Param{Key: "dd", Value: "d"}, Param{Key: "ee", Value: "e"}, Param{Key: "ff", Value: "f"}, Param{Key: "gg", Value: "g"}}},
 	})
 
 	checkPriorities(t, tree)