Repository: labstack/echo
Pull request #2616 (2024-03-24 21:31:10)

When route is registered with empty path it is normalized to `/`. 
When route is registered with empty path it is normalized to `/`. Make sure that returned echo.Route structs reflect that behavior.  Internally router has changed `` (empty path) to `/` for a long time but Route that is returned did not reflect that. Is is problematic with `Reverse` function that uses empty string as "not found"

Related to #2615

Previous behavior can be seen with this test:
```go
func TestTest(t *testing.T) {
	e := echo.New()

	handler := func(c echo.Context) error {
		return c.String(http.StatusNotImplemented, "Nope")
	}
	r := e.GET("", handler) // path is registered as "" previously. After change `/` is registered
	r.Name = "test"

	existingEmpty := e.Reverse("test")
	assert.Equal(t, "", existingEmpty)

	notExistingEmpty := e.Reverse("not-existing")
	assert.Equal(t, "", notExistingEmpty)
}
```

whis this change `assert.Equal(t, "", existingEmpty)` shoulb be change to  `assert.Equal(t, "/", existingEmpty)`to pass the test

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/labstack/echo/pull/2616?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 93.03%. Comparing base [(`d549290`)](https://app.codecov.io/gh/labstack/echo/commit/d549290448fc65ae075f0b960b25489f6a38cb78?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack) to head [(`65d0914`)](https://app.codecov.io/gh/labstack/echo/pull/2616?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #2616   +/-   ##
=======================================
  Coverage   93.02%   93.03%           
=======================================
  Files          41       41           
  Lines        4703     4707    +4     
=======================================
+ Hits         4375     4379    +4     
  Misses        236      236           
  Partials       92       92           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/labstack/echo/pull/2616?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=labstack).


Patch:
diff --git a/router.go b/router.go
index a3e258e..0326731 100644
--- a/router.go
+++ b/router.go
@@ -185,8 +185,18 @@ func (r *Router) Reverse(name string, params ...interface{}) string {
 	return uri.String()
 }
 
+func normalizePathSlash(path string) string {
+	if path == "" {
+		path = "/"
+	} else if path[0] != '/' {
+		path = "/" + path
+	}
+	return path
+}
+
 func (r *Router) add(method, path, name string, h HandlerFunc) *Route {
-	r.Add(method, path, h)
+	path = normalizePathSlash(path)
+	r.insert(method, path, h)
 
 	route := &Route{
 		Method: method,
@@ -199,13 +209,11 @@ func (r *Router) add(method, path, name string, h HandlerFunc) *Route {
 
 // Add registers a new route for method and path with matching handler.
 func (r *Router) Add(method, path string, h HandlerFunc) {
-	// Validate path
-	if path == "" {
-		path = "/"
-	}
-	if path[0] != '/' {
-		path = "/" + path
-	}
+	r.insert(method, normalizePathSlash(path), h)
+}
+
+func (r *Router) insert(method, path string, h HandlerFunc) {
+	path = normalizePathSlash(path)
 	pnames := []string{} // Param names
 	ppath := path        // Pristine path
 
@@ -224,7 +232,7 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
 			}
 			j := i + 1
 
-			r.insert(method, path[:i], staticKind, routeMethod{})
+			r.insertNode(method, path[:i], staticKind, routeMethod{})
 			for ; i < lcpIndex && path[i] != '/'; i++ {
 			}
 
@@ -234,21 +242,21 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
 
 			if i == lcpIndex {
 				// path node is last fragment of route path. ie. `/users/:id`
-				r.insert(method, path[:i], paramKind, routeMethod{ppath, pnames, h})
+				r.insertNode(method, path[:i], paramKind, routeMethod{ppath, pnames, h})
 			} else {
-				r.insert(method, path[:i], paramKind, routeMethod{})
+				r.insertNode(method, path[:i], paramKind, routeMethod{})
 			}
 		} else if path[i] == '*' {
-			r.insert(method, path[:i], staticKind, routeMethod{})
+			r.insertNode(method, path[:i], staticKind, routeMethod{})
 			pnames = append(pnames, "*")
-			r.insert(method, path[:i+1], anyKind, routeMethod{ppath, pnames, h})
+			r.insertNode(method, path[:i+1], anyKind, routeMethod{ppath, pnames, h})
 		}
 	}
 
-	r.insert(method, path, staticKind, routeMethod{ppath, pnames, h})
+	r.insertNode(method, path, staticKind, routeMethod{ppath, pnames, h})
 }
 
-func (r *Router) insert(method, path string, t kind, rm routeMethod) {
+func (r *Router) insertNode(method, path string, t kind, rm routeMethod) {
 	// Adjust max param
 	paramLen := len(rm.pnames)
 	if *r.echo.maxParam < paramLen {


Test patch:
diff --git a/echo_test.go b/echo_test.go
index 57c257b..b7f3201 100644
--- a/echo_test.go
+++ b/echo_test.go
@@ -1598,6 +1598,11 @@ func TestEchoReverse(t *testing.T) {
 		whenParams    []interface{}
 		expect        string
 	}{
+		{
+			name:          "ok, not existing path returns empty url",
+			whenRouteName: "not-existing",
+			expect:        "",
+		},
 		{
 			name:          "ok,static with no params",
 			whenRouteName: "/static",
diff --git a/router_test.go b/router_test.go
index 52d9a0a..dca0d47 100644
--- a/router_test.go
+++ b/router_test.go
@@ -2770,6 +2770,22 @@ func TestRouter_Routes(t *testing.T) {
 	}
 }
 
+func TestRouter_addEmptyPathToSlashReverse(t *testing.T) {
+	e := New()
+	r := e.router
+	r.add(http.MethodGet, "", "empty", handlerFunc) // emtpy path is normalized to `/`
+
+	assert.Equal(t, "/", r.Reverse("empty"))
+}
+
+func TestRouter_ReverseNotFound(t *testing.T) {
+	e := New()
+	r := e.router
+	r.add(http.MethodGet, "", "empty", handlerFunc)
+
+	assert.Equal(t, "", r.Reverse("not-existing"))
+}
+
 func TestRouter_Reverse(t *testing.T) {
 	e := New()
 	r := e.router