From 4da962d339be92344e99b958f9d15c5de0de83fd Mon Sep 17 00:00:00 2001 From: themavik Date: Thu, 19 Mar 2026 08:38:27 -0400 Subject: [PATCH] fix: prefer exact path match over parameterized match for method checking Fixes #2547 Made-with: Cursor --- router.go | 10 ++++++++++ router_test.go | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) 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) {