From ca34ff9b5c286c60a7ca4ed34a0d35a3ff7a58a1 Mon Sep 17 00:00:00 2001 From: majiayu000 <1835304752@qq.com> Date: Thu, 1 Jan 2026 17:06:12 +0800 Subject: [PATCH 1/2] fix: RouteNotFound handler fallback to root handler for groups with middleware When a group has middleware, the RouteNotFound handler registered at the root level is now properly used as a fallback. Previously, groups with middleware would always use the default NotFoundHandler instead of falling back to the root's custom RouteNotFound handler. This fix: - Adds a routeNotFoundHandler field to Echo to store the root handler - Modifies group.Use() to use a fallback handler that checks for the root's RouteNotFound handler at request time - Updates tests to reflect the new expected behavior Fixes #2485 Signed-off-by: majiayu000 <1835304752@qq.com> --- echo.go | 8 ++++++++ group.go | 13 +++++++++++-- group_test.go | 30 ++++++++++++++++++++++++++---- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/echo.go b/echo.go index 7e440d37f..53211f794 100644 --- a/echo.go +++ b/echo.go @@ -105,6 +105,10 @@ type Echo struct { Debug bool HideBanner bool HidePort bool + + // routeNotFoundHandler is the handler registered via RouteNotFound for "/*" path. + // It is used as a fallback for groups with middleware that don't have their own RouteNotFound handler. + routeNotFoundHandler HandlerFunc } // Route contains a handler and information for matching against requests. @@ -542,6 +546,10 @@ func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { // // Example: `e.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })` func (e *Echo) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { + // Store the handler for "/*" path so it can be used as fallback for groups with middleware + if path == "/*" || path == "*" { + e.routeNotFoundHandler = h + } return e.Add(RouteNotFound, path, h, m...) } diff --git a/group.go b/group.go index cb37b123f..af122447b 100644 --- a/group.go +++ b/group.go @@ -28,8 +28,17 @@ func (g *Group) Use(middleware ...MiddlewareFunc) { // are only executed if they are added to the Router with route. // So we register catch all route (404 is a safe way to emulate route match) for this group and now during routing the // Router would find route to match our request path and therefore guarantee the middleware(s) will get executed. - g.RouteNotFound("", NotFoundHandler) - g.RouteNotFound("/*", NotFoundHandler) + // + // We use a fallback handler that checks for a root-level RouteNotFound handler at request time. + // This allows the root's custom 404 handler to be used as fallback for groups. + fallbackNotFoundHandler := func(c Context) error { + if g.echo.routeNotFoundHandler != nil { + return g.echo.routeNotFoundHandler(c) + } + return NotFoundHandler(c) + } + g.RouteNotFound("", fallbackNotFoundHandler) + g.RouteNotFound("/*", fallbackNotFoundHandler) } // CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. diff --git a/group_test.go b/group_test.go index a97371418..4caf3ff89 100644 --- a/group_test.go +++ b/group_test.go @@ -204,17 +204,17 @@ func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) { expectCode: http.StatusNotFound, }, { - name: "ok, default group 404 handler is called with middleware", + name: "ok, root 404 handler is used as fallback with middleware", givenCustom404: false, whenURL: "/group/test3", - expectBody: "{\"message\":\"Not Found\"}\n", + expectBody: "GET /group/*", expectCode: http.StatusNotFound, }, { - name: "ok, (no slash) default group 404 handler is called with middleware", + name: "ok, (no slash) root 404 handler is used as fallback with middleware", givenCustom404: false, whenURL: "/group", - expectBody: "{\"message\":\"Not Found\"}\n", + expectBody: "GET /group", expectCode: http.StatusNotFound, }, } @@ -257,3 +257,25 @@ func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) { }) } } + +func TestGroup_RouteNotFoundWithMiddleware_NoRootHandler(t *testing.T) { + e := New() + + middlewareCalled := false + g := e.Group("/group") + g.Use(func(next HandlerFunc) HandlerFunc { + return func(c Context) error { + middlewareCalled = true + return next(c) + } + }) + + req := httptest.NewRequest(http.MethodGet, "/group/unknown", nil) + rec := httptest.NewRecorder() + + e.ServeHTTP(rec, req) + + assert.True(t, middlewareCalled) + assert.Equal(t, http.StatusNotFound, rec.Code) + assert.Equal(t, "{\"message\":\"Not Found\"}\n", rec.Body.String()) +} From b5ef32ff67165335834562179805013f07088e8c Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Thu, 1 Jan 2026 19:37:05 +0800 Subject: [PATCH 2/2] docs: clarify NotFoundHandler for group middleware --- echo.go | 11 +++-------- group.go | 13 ++----------- group_test.go | 30 ++++-------------------------- 3 files changed, 9 insertions(+), 45 deletions(-) diff --git a/echo.go b/echo.go index 53211f794..6823fc876 100644 --- a/echo.go +++ b/echo.go @@ -105,10 +105,6 @@ type Echo struct { Debug bool HideBanner bool HidePort bool - - // routeNotFoundHandler is the handler registered via RouteNotFound for "/*" path. - // It is used as a fallback for groups with middleware that don't have their own RouteNotFound handler. - routeNotFoundHandler HandlerFunc } // Route contains a handler and information for matching against requests. @@ -353,6 +349,9 @@ var ( // NotFoundHandler is the handler that router uses in case there was no matching route found. Returns an error that results // HTTP 404 status code. +// +// Note: Group-level middleware registers catch-all routes using NotFoundHandler to ensure the middleware chain executes. +// If you want a custom 404 for those groups, override NotFoundHandler or register a group RouteNotFound handler. var NotFoundHandler = func(c Context) error { return ErrNotFound } @@ -546,10 +545,6 @@ func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { // // Example: `e.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })` func (e *Echo) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) *Route { - // Store the handler for "/*" path so it can be used as fallback for groups with middleware - if path == "/*" || path == "*" { - e.routeNotFoundHandler = h - } return e.Add(RouteNotFound, path, h, m...) } diff --git a/group.go b/group.go index af122447b..cb37b123f 100644 --- a/group.go +++ b/group.go @@ -28,17 +28,8 @@ func (g *Group) Use(middleware ...MiddlewareFunc) { // are only executed if they are added to the Router with route. // So we register catch all route (404 is a safe way to emulate route match) for this group and now during routing the // Router would find route to match our request path and therefore guarantee the middleware(s) will get executed. - // - // We use a fallback handler that checks for a root-level RouteNotFound handler at request time. - // This allows the root's custom 404 handler to be used as fallback for groups. - fallbackNotFoundHandler := func(c Context) error { - if g.echo.routeNotFoundHandler != nil { - return g.echo.routeNotFoundHandler(c) - } - return NotFoundHandler(c) - } - g.RouteNotFound("", fallbackNotFoundHandler) - g.RouteNotFound("/*", fallbackNotFoundHandler) + g.RouteNotFound("", NotFoundHandler) + g.RouteNotFound("/*", NotFoundHandler) } // CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. diff --git a/group_test.go b/group_test.go index 4caf3ff89..a97371418 100644 --- a/group_test.go +++ b/group_test.go @@ -204,17 +204,17 @@ func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) { expectCode: http.StatusNotFound, }, { - name: "ok, root 404 handler is used as fallback with middleware", + name: "ok, default group 404 handler is called with middleware", givenCustom404: false, whenURL: "/group/test3", - expectBody: "GET /group/*", + expectBody: "{\"message\":\"Not Found\"}\n", expectCode: http.StatusNotFound, }, { - name: "ok, (no slash) root 404 handler is used as fallback with middleware", + name: "ok, (no slash) default group 404 handler is called with middleware", givenCustom404: false, whenURL: "/group", - expectBody: "GET /group", + expectBody: "{\"message\":\"Not Found\"}\n", expectCode: http.StatusNotFound, }, } @@ -257,25 +257,3 @@ func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) { }) } } - -func TestGroup_RouteNotFoundWithMiddleware_NoRootHandler(t *testing.T) { - e := New() - - middlewareCalled := false - g := e.Group("/group") - g.Use(func(next HandlerFunc) HandlerFunc { - return func(c Context) error { - middlewareCalled = true - return next(c) - } - }) - - req := httptest.NewRequest(http.MethodGet, "/group/unknown", nil) - rec := httptest.NewRecorder() - - e.ServeHTTP(rec, req) - - assert.True(t, middlewareCalled) - assert.Equal(t, http.StatusNotFound, rec.Code) - assert.Equal(t, "{\"message\":\"Not Found\"}\n", rec.Body.String()) -}