Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -1011,12 +1011,28 @@ func (r *DefaultRouter) Route(c *Context) HandlerFunc {
rPath = matchedRouteMethod.Path
rHandler = matchedRouteMethod.handler
} else if currentNode.isHandler {
rInfo = methodNotAllowedRouteInfo
// Walk up the tree to find a parent's notFoundHandler
// This allows RouteNotFound handlers defined at a higher level in the tree
// (e.g., root level) to handle 404s for sub-paths (e.g., groups)
var parentNode *node
for parentNode = currentNode.parent; parentNode != nil; parentNode = parentNode.parent {
if parentNode.methods.notFoundHandler != nil {
matchedRouteMethod = parentNode.methods.notFoundHandler
rInfo = matchedRouteMethod.RouteInfo
rPath = matchedRouteMethod.Path
rHandler = matchedRouteMethod.handler
break
}
}

c.Set(ContextKeyHeaderAllow, currentNode.methods.allowHeader)
rHandler = r.methodNotAllowedHandler
if req.Method == http.MethodOptions {
rHandler = r.optionsMethodHandler
// If no parent notFoundHandler was found, use methodNotAllowedHandler
if rHandler == nil {
rInfo = methodNotAllowedRouteInfo
c.Set(ContextKeyHeaderAllow, currentNode.methods.allowHeader)
rHandler = r.methodNotAllowedHandler
if req.Method == http.MethodOptions {
rHandler = r.optionsMethodHandler
}
}
}
}
Expand Down
36 changes: 36 additions & 0 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3656,3 +3656,39 @@ func BenchmarkRouterGooglePlusAPIMisses(b *testing.B) {
func BenchmarkRouterParamsAndAnyAPI(b *testing.B) {
benchmarkRouterRoutes(b, paramAndAnyAPI, paramAndAnyAPIToFind)
}

// TestRouterRouteNotFoundParentWalkBug verifies the fix for issue #2485
// This test should FAIL on unpatched code and PASS with the fix
func TestRouterRouteNotFoundParentWalkBug(t *testing.T) {
e := New()
handlerID := ""

// Register a GET route inside a group
g := e.Group("/api")
g.GET("/users", func(c *Context) error {
handlerID = "users-get"
return nil
})

// Register RouteNotFound on the root at exact prefix "/api" (no wildcard)
// This is the critical scenario: without parent-walk fix, this handler won't be found
e.RouteNotFound("/api", func(c *Context) error {
handlerID = "root-not-found"
return nil
})

// Test case: request exists in router but with wrong HTTP method
// Without fix: should fall back to methodNotAllowedHandler (405)
// With fix: should find parent's RouteNotFound handler
req := httptest.NewRequest(http.MethodPost, "/api/users", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
handler := e.router.Route(c)

_ = handler(c)

// This assertion should FAIL on unpatched code (handlerID will be empty)
// and PASS with the fix (handlerID will be "root-not-found")
assert.Equal(t, "root-not-found", handlerID,
"Expected root RouteNotFound handler to fire for POST /api/users")
}