`/hello/:helloID/world` is matching `/hello//world` when I was not expecting that
Issue Description
Route /hello/:helloID/world is matching /hello//world when I was not expecting that.
Checklist
- [X] Dependencies installed
- [X] No typos
- [X] Searched existing issues and docs
Expected behaviour
I was expecting to get 404 like it happens when I call hello3/ and the closest available route I have is /hello3/:helloID.
Actual behaviour
/hello//world is reaching route /hello/:helloID/world.
Steps to reproduce
See example below.
Working code to debug
package main
import (
"context"
"fmt"
"io"
"net/http"
"sync"
"time"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.GET("/hello/:helloID/world", func(c echo.Context) error {
return c.String(http.StatusOK, fmt.Sprintf("Hello, World %v!", c.Param("helloID")))
})
g := e.Group("/test")
g.GET("/hello2/:helloID/world", func(c echo.Context) error {
return c.String(http.StatusOK, fmt.Sprintf("Hello2, World %v!", c.Param("helloID2")))
})
e.GET("/hello3/:helloID", func(c echo.Context) error {
return c.String(http.StatusOK, fmt.Sprintf("Hello, World %v!", c.Param("helloID")))
})
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println(e.Start(":1323"))
}()
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(time.Second * 1)
test("http://localhost:1323/hello//world")
test("http://localhost:1323/test/hello2//world")
test("http://localhost:1323/hello3/")
if err := e.Shutdown(context.Background()); err != nil {
fmt.Println(err)
}
}()
wg.Wait()
fmt.Println("Finished!")
}
func test(url string) {
var client http.Client
response, _ := client.Get(url)
body, _ := io.ReadAll(response.Body)
fmt.Println(string(body))
}
Output:
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.7.2
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
O\
⇨ http server started on [::]:1323
Hello, World !
Hello2, World !
{"message":"Not Found"}
http: Server closed
Finished!
Program exited.
Version/commit
Imported github.com/labstack/echo/v4 into my program.
This is quite implementation related and changing how it works probably would break applications that have route pairs likes
-
/users/<-- usually these routes list something -
/users/:id<-- these routes show details about that entity
making second one match first one would break existing stuff
some notes about Echo router internals. When these routes are added we create routing three likes that
1. "/" (static)
1.1. "hello" (static)
1.1.1. "/" (static)
1.1.1.1. ":" (param) (isHandler)
1.1.2. "3/" (static)
1.1.2.1. ":" (param) (isHandler)
1.2. "test/hello2/"
1.2.1. ":" (param)
1.2.1.1. "/world" (static) (isHandler)
┌───┐
│ / ├─────────┐
└─┬─┘ │
│ │
┌──────────▼─┐ ┌▼────┐
│test/hello2/│ │hello│
└┬───────────┘ └┬────┤
│ │ │
┌▼┐ ┌▼┐ │ ┌──┐
│:│ │/│ └─►3/│
└┬┘ └┬┘ └┬─┘
│ │ │
┌▼─────┐ ┌▼┐ ┌▼┐
│/world│ │:│ │:│
└──────┘ └─┘ └┬┘
│
┌▼─────┐
│/world│
└──────┘
Only those with isHandler have handler attached are able to serve handler functions (unless there is some middleware that can alter the chain).
When "param" is at the end of route path we do not consider empty string `` as match
When "param" is not at the end of the route we consider empty string as match for parameter.
This relates how we move down in the three and search matches for request path. When we reach at the end of the request path and we are currently in three at some node that is not handler we do not check is there is child node that could accept empty string as match (any/param routes)
I assume this param behavior is not intentional, but it is the way it works.
Until there is no resolution for this issue I recommend adding another route /hello3/ that has same handler function has /hello3/:helloID route
Closing, as no further comments came up.