Repository: labstack/echo (go)

Issue:
Routing Bug or Feature?
### Issue Description

Before v4.1.15 these routes used to satisfy both "/articles", "/articles/" and "/articles/whatever" URLs.
```
		"/articles",
		"/articles/*",
```
### Checklist

- [X] Dependencies installed
- [X] No typos
- [X] Searched existing issues and docs

### Expected behaviour
```
		"/articles",
		"/articles/*",
```
satisfy both "/articles", "/articles/" and "/articles/whatever" URLs.

### Actual behaviour

Since v4.1.15 now the list of routes to satisfy required behavior is this:
```
		"/articles",
		"/articles/",
		"/articles/*",
```
### Version/commit

v4.1.15


Please confirm whether this is a new behavior or bug or there's a better way of handling this scenario.

Pull request description:
Fix #1526 trailing slash to any route
This PR fixes #1526 to handle a corner case for a request with trailing slash that shall match an any route just below the request path.

`GET /users/` will now correctly match the any route `/users/*`


Patch:
diff --git a/router.go b/router.go
index 15a3398..ed728d6 100644
--- a/router.go
+++ b/router.go
@@ -355,6 +355,10 @@ func (r *Router) Find(method, path string, c Context) {
 
 		// Attempt to go back up the tree on no matching prefix or no remaining search
 		if l != pl || search == "" {
+			// Handle special case of trailing slash route with existing any route (see #1526)
+			if path[len(path)-1] == '/' && cn.findChildByKind(akind) != nil {
+				goto Any
+			}
 			if nn == nil { // Issue #1348
 				return // Not found
 			}


Test patch:
diff --git a/router_test.go b/router_test.go
index 8c27b9f..0e88323 100644
--- a/router_test.go
+++ b/router_test.go
@@ -608,7 +608,6 @@ func TestRouterMatchAny(t *testing.T) {
 		return nil
 	})
 	c := e.NewContext(nil, nil).(*context)
-
 	r.Find(http.MethodGet, "/", c)
 	assert.Equal(t, "", c.Param("*"))
 
@@ -619,6 +618,78 @@ func TestRouterMatchAny(t *testing.T) {
 	assert.Equal(t, "joe", c.Param("*"))
 }
 
+// TestRouterMatchAnySlash shall verify finding the best route
+// for any routes with trailing slash requests
+func TestRouterMatchAnySlash(t *testing.T) {
+	e := New()
+	r := e.router
+
+	handler := func(c Context) error {
+		c.Set("path", c.Path())
+		return nil
+	}
+
+	// Routes
+	r.Add(http.MethodGet, "/users", handler)
+	r.Add(http.MethodGet, "/users/*", handler)
+	r.Add(http.MethodGet, "/img/*", handler)
+	r.Add(http.MethodGet, "/img/load", handler)
+	r.Add(http.MethodGet, "/img/load/*", handler)
+	r.Add(http.MethodGet, "/assets/*", handler)
+
+	c := e.NewContext(nil, nil).(*context)
+	r.Find(http.MethodGet, "/", c)
+	assert.Equal(t, "", c.Param("*"))
+
+	// Test trailing slash request for simple any route (see #1526)
+	c = e.NewContext(nil, nil).(*context)
+	r.Find(http.MethodGet, "/users/", c)
+	c.handler(c)
+	assert.Equal(t, "/users/*", c.Get("path"))
+	assert.Equal(t, "", c.Param("*"))
+
+	c = e.NewContext(nil, nil).(*context)
+	r.Find(http.MethodGet, "/users/joe", c)
+	c.handler(c)
+	assert.Equal(t, "/users/*", c.Get("path"))
+	assert.Equal(t, "joe", c.Param("*"))
+
+	// Test trailing slash request for nested any route (see #1526)
+	c = e.NewContext(nil, nil).(*context)
+	r.Find(http.MethodGet, "/img/load", c)
+	c.handler(c)
+	assert.Equal(t, "/img/load", c.Get("path"))
+	assert.Equal(t, "", c.Param("*"))
+
+	c = e.NewContext(nil, nil).(*context)
+	r.Find(http.MethodGet, "/img/load/", c)
+	c.handler(c)
+	assert.Equal(t, "/img/load/*", c.Get("path"))
+	assert.Equal(t, "", c.Param("*"))
+
+	c = e.NewContext(nil, nil).(*context)
+	r.Find(http.MethodGet, "/img/load/ben", c)
+	c.handler(c)
+	assert.Equal(t, "/img/load/*", c.Get("path"))
+	assert.Equal(t, "ben", c.Param("*"))
+
+	// Test /assets/* any route
+	// ... without trailing slash must not match
+	c = e.NewContext(nil, nil).(*context)
+	r.Find(http.MethodGet, "/assets", c)
+	c.handler(c)
+	assert.Equal(t, nil, c.Get("path"))
+	assert.Equal(t, "", c.Param("*"))
+
+	// ... with trailing slash must match
+	c = e.NewContext(nil, nil).(*context)
+	r.Find(http.MethodGet, "/assets/", c)
+	c.handler(c)
+	assert.Equal(t, "/assets/*", c.Get("path"))
+	assert.Equal(t, "", c.Param("*"))
+
+}
+
 func TestRouterMatchAnyMultiLevel(t *testing.T) {
 	e := New()
 	r := e.router