diff --git a/router.go b/router.go index 0db3bc04c..b352c951b 100644 --- a/router.go +++ b/router.go @@ -911,6 +911,16 @@ func (r *DefaultRouter) Route(c *Context) HandlerFunc { } else if currentNode.methods.notFoundHandler != nil { matchedRouteMethod = currentNode.methods.notFoundHandler break + } else if currentNode.paramChild != nil && currentNode.anyChild == nil && + currentNode.parent != nil && currentNode.parent.paramChild != nil { + // Path exactly matches this static node. Prefer this over backtracking to a param route + // that would match the last segment (e.g. POST /VerifiedCallerId/Verification should not + // match GET /VerifiedCallerId/:phone_number). Only when parent has paramChild (backtrack + // would match) - otherwise we'd return 405 for paths that should be 404 (e.g. /a3 with route /a3/:id). + if previousBestMatchNode == nil { + previousBestMatchNode = currentNode.paramChild + } + break } } diff --git a/router_test.go b/router_test.go index 7bddb4a15..8826e1ddc 100644 --- a/router_test.go +++ b/router_test.go @@ -1514,6 +1514,25 @@ func TestRouterMatchAnyPrefixIssue(t *testing.T) { } } +// TestRouterRoutePreferExactPathOverParam tests issue #2547: POST to path without param +// should return 405 with Allow: POST (from the more specific route), not 405 from +// the param route that would capture the last segment. +func TestRouterRoutePreferExactPathOverParam(t *testing.T) { + e := New() + e.GET("/VerifiedCallerId/:phone_number", handlerFunc) + e.POST("/VerifiedCallerId/Verification/:verification_uuid", handlerFunc) + + req := httptest.NewRequest(http.MethodPost, "/VerifiedCallerId/Verification/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + handler := e.router.Route(c) + + err := handler(c) + assert.ErrorIs(t, err, ErrMethodNotAllowed) + assert.Contains(t, rec.Header().Get("Allow"), "POST") + assert.NotContains(t, rec.Header().Get("Allow"), "GET") +} + // TestRouterMatchAnySlash shall verify finding the best route // for any routes with trailing slash requests func TestRouterMatchAnySlash(t *testing.T) {