Repository: gin-gonic/gin (go)

Issue:
Incorrect TSR with mixed static and wildcard paths
The backtracking logic in #2897 is causing incorrect TSR values:
```go
func TestTreeBacktrackingTsr(t *testing.T) {
	tree := &node{}
	tree.addRoute("/api/:page/:name", fakeHandler("1"))
	tree.addRoute("/api/hello/:name/bar/", fakeHandler("1"))
	res := tree.getValue("/api/hello/x/bar", getParams(), getSkippedNodes(), false)
	if res.tsr != true {
		t.Fatalf("expected tsr")
	}
}
```
```
--- FAIL: TestTreeBacktrackingTsr (0.00s)
    tree_test.go:855: expected tsr
```

I believe the TSR would have to be checked before backtracking.

Pull request description:
fix tsr with mixed static and wildcard paths
Resolves #2918

Patch:
diff --git a/errors.go b/errors.go
index 0f276c1..3418cbc 100644
--- a/errors.go
+++ b/errors.go
@@ -122,7 +122,7 @@ func (a errorMsgs) Last() *Error {
 	return nil
 }
 
-// Errors returns an array will all the error messages.
+// Errors returns an array with all the error messages.
 // Example:
 // 		c.Error(errors.New("first"))
 // 		c.Error(errors.New("second"))
diff --git a/tree.go b/tree.go
index a0d9835..2f0de1a 100644
--- a/tree.go
+++ b/tree.go
@@ -447,27 +447,26 @@ walk: // Outer loop for walking the tree
 						continue walk
 					}
 				}
-				// If the path at the end of the loop is not equal to '/' and the current node has no child nodes
-				// the current node needs to roll back to last vaild skippedNode
-
-				if path != "/" && !n.wildChild {
-					for l := len(*skippedNodes); l > 0; {
-						skippedNode := (*skippedNodes)[l-1]
-						*skippedNodes = (*skippedNodes)[:l-1]
-						if strings.HasSuffix(skippedNode.path, path) {
-							path = skippedNode.path
-							n = skippedNode.node
-							if value.params != nil {
-								*value.params = (*value.params)[:skippedNode.paramsCount]
+
+				if !n.wildChild {
+					// If the path at the end of the loop is not equal to '/' and the current node has no child nodes
+					// the current node needs to roll back to last vaild skippedNode
+					if path != "/" {
+						for l := len(*skippedNodes); l > 0; {
+							skippedNode := (*skippedNodes)[l-1]
+							*skippedNodes = (*skippedNodes)[:l-1]
+							if strings.HasSuffix(skippedNode.path, path) {
+								path = skippedNode.path
+								n = skippedNode.node
+								if value.params != nil {
+									*value.params = (*value.params)[:skippedNode.paramsCount]
+								}
+								globalParamsCount = skippedNode.paramsCount
+								continue walk
 							}
-							globalParamsCount = skippedNode.paramsCount
-							continue walk
 						}
 					}
-				}
 
-				// 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.
@@ -614,8 +613,14 @@ walk: // Outer loop for walking the tree
 			return
 		}
 
-		// roll back to last vaild skippedNode
-		if path != "/" {
+		// 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)
+
+		// roll back to last valid skippedNode
+		if !value.tsr && path != "/" {
 			for l := len(*skippedNodes); l > 0; {
 				skippedNode := (*skippedNodes)[l-1]
 				*skippedNodes = (*skippedNodes)[:l-1]
@@ -631,11 +636,6 @@ walk: // Outer loop for walking the tree
 			}
 		}
 
-		// 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)
 		return
 	}
 }


Test patch:
diff --git a/tree_test.go b/tree_test.go
index 49b3b57..c372339 100644
--- a/tree_test.go
+++ b/tree_test.go
@@ -587,7 +587,14 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
 		"/doc/go1.html",
 		"/no/a",
 		"/no/b",
-		"/api/hello/:name",
+		"/api/:page/:name",
+		"/api/hello/:name/bar/",
+		"/api/bar/:name",
+		"/api/baz/foo",
+		"/api/baz/foo/bar",
+		"/blog/:p",
+		"/posts/:b/:c",
+		"/posts/b/:c/d/",
 	}
 	for _, route := range routes {
 		recv := catchPanic(func() {
@@ -613,7 +620,19 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
 		"/admin/config/",
 		"/admin/config/permissions/",
 		"/doc/",
+		"/admin/static/",
+		"/admin/cfg/",
+		"/admin/cfg/users/",
+		"/api/hello/x/bar",
+		"/api/baz/foo/",
+		"/api/baz/bax/",
+		"/api/bar/huh/",
+		"/api/baz/foo/bar/",
+		"/api/world/abc/",
+		"/blog/pp/",
+		"/posts/b/c/d",
 	}
+
 	for _, route := range tsrRoutes {
 		value := tree.getValue(route, nil, getSkippedNodes(), false)
 		if value.handlers != nil {
@@ -629,7 +648,11 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
 		"/no/",
 		"/_",
 		"/_/",
-		"/api/world/abc",
+		"/api",
+		"/api/",
+		"/api/hello/x/foo",
+		"/api/baz/foo/bad",
+		"/foo/p/p",
 	}
 	for _, route := range noTsrRoutes {
 		value := tree.getValue(route, nil, getSkippedNodes(), false)