Skip to content

Commit 2d9b2aa

Browse files
committed
Revert back to v4 behavior for group registering implicit 404 handlers. This will fix: CORS middleware doesnt automatically handle OPTIONS routes for groups anymore since upgrade to v5
1 parent 222be90 commit 2d9b2aa

6 files changed

Lines changed: 141 additions & 37 deletions

File tree

echo.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ type Echo struct {
110110
// formParseMaxMemory is passed to Context for multipart form parsing (See http.Request.ParseMultipartForm)
111111
formParseMaxMemory int64
112112

113+
// noGroupAutoRegisterRoutes is a flag that indicates whether echo.Group should NOT register 404 routes automatically
114+
// when there are middlewares registered with the group.
115+
noGroupAutoRegisterRoutes bool
116+
113117
enablePathUnescapingStaticFiles bool
114118
}
115119

@@ -327,6 +331,12 @@ type Config struct {
327331
//
328332
// Applies to methods: Echo.Static, Echo.StaticFS, Group.Static, Group.StaticFS.
329333
EnablePathUnescapingStaticFiles bool
334+
335+
// NoGroupAutoRegister404Routes bool is a flag that indicates whether echo.Group should NOT register 404 routes automatically
336+
// when there are middlewares registered with the group.
337+
// Note: if you decide not to register 404 routes automatically, make sure to check if all your middlewares are executed
338+
// as expected. For example - CORS middleware.
339+
NoGroupAutoRegister404Routes bool
330340
}
331341

332342
// NewWithConfig creates an instance of Echo with given configuration.
@@ -367,6 +377,8 @@ func NewWithConfig(config Config) *Echo {
367377
}
368378
e.enablePathUnescapingStaticFiles = config.EnablePathUnescapingStaticFiles
369379

380+
e.noGroupAutoRegisterRoutes = config.NoGroupAutoRegister404Routes
381+
370382
return e
371383
}
372384

@@ -383,7 +395,9 @@ func New() *Echo {
383395
}
384396

385397
e.serveHTTPFunc = e.serveHTTP
386-
e.router = NewRouter(RouterConfig{})
398+
e.router = NewRouter(RouterConfig{
399+
AllowOverwritingRoute: true,
400+
})
387401
e.HTTPErrorHandler = DefaultHTTPErrorHandler(false)
388402
e.contextPool.New = func() any {
389403
return newContext(nil, nil, e)
@@ -737,7 +751,11 @@ func (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...Middl
737751

738752
// Group creates a new router group with prefix and optional group-level middleware.
739753
func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) {
740-
g = &Group{prefix: prefix, echo: e}
754+
g = &Group{
755+
prefix: prefix,
756+
echo: e,
757+
noAutoRegisterRoutes: e.noGroupAutoRegisterRoutes,
758+
}
741759
g.Use(m...)
742760
return
743761
}

echo_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,12 +1014,12 @@ func TestEchoServeHTTPPathEncoding(t *testing.T) {
10141014
func TestEchoGroup(t *testing.T) {
10151015
e := New()
10161016
buf := new(bytes.Buffer)
1017-
e.Use(MiddlewareFunc(func(next HandlerFunc) HandlerFunc {
1017+
e.Use(func(next HandlerFunc) HandlerFunc {
10181018
return func(c *Context) error {
10191019
buf.WriteString("0")
10201020
return next(c)
10211021
}
1022-
}))
1022+
})
10231023
h := func(c *Context) error {
10241024
return c.NoContent(http.StatusOK)
10251025
}

group.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,34 @@ type Group struct {
1515
echo *Echo
1616
prefix string
1717
middleware []MiddlewareFunc
18+
19+
// noAutoRegisterRoutes is a flag that indicates whether Group should NOT register 404 routes automatically
20+
// when there are middlewares registered with the group.
21+
// Note: if you decide not to register 404 routes automatically, make sure to check if all your middlewares are executed
22+
// as expected. For example - CORS middleware.
23+
noAutoRegisterRoutes bool
1824
}
1925

2026
// Use implements `Echo#Use()` for sub-routes within the Group.
21-
// Group middlewares are not executed on request when there is no matching route found.
27+
//
28+
// Important! Group middlewares are executed in case there was no exact route match as by default Group registers
29+
// `/*` NotFound routes for itself. If this kind of behavior is not needed, then create an Echo instance with the ` noAutoRegisterRoutes `
30+
// flag set to true. Example `echo.NewWithConfig(echo.Config{NoGroupAutoRegister404Routes: true})`.
2231
func (g *Group) Use(middleware ...MiddlewareFunc) {
2332
g.middleware = append(g.middleware, middleware...)
33+
if len(g.middleware) == 0 {
34+
return
35+
}
36+
if g.noAutoRegisterRoutes {
37+
return
38+
}
39+
// group level middlewares are different from Echo `Pre` and `Use` middlewares (those are global). Group level middlewares
40+
// are only executed if they are added to the Router with route.
41+
// So we register catch all route (404 is a safe way to emulate route match) for this group and now during routing the
42+
// Router would find route to match our request path and therefore guarantee the middleware(s) will get executed.
43+
// Note: we use nil handler so Router would choose the default 404 handler. This may not work with custom routers.
44+
g.RouteNotFound("", nil)
45+
g.RouteNotFound("/*", nil)
2446
}
2547

2648
// CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. Panics on error.
@@ -102,9 +124,10 @@ func (g *Group) Match(methods []string, path string, handler HandlerFunc, middle
102124
}
103125

104126
// Group creates a new sub-group with prefix and optional sub-group-level middleware.
105-
// Important! Group middlewares are only executed in case there was exact route match and not
106-
// for 404 (not found) or 405 (method not allowed) cases. If this kind of behaviour is needed then add
107-
// a catch-all route `/*` for the group which handler returns always 404
127+
//
128+
// Important! Group middlewares are executed in case there was no exact route match as by default Group registers
129+
// `/*` NotFound routes for itself. If this kind of behavior is not needed, then create an Echo instance with the ` noAutoRegisterRoutes `
130+
// flag set to true. Example `echo.NewWithConfig(echo.Config{NoGroupAutoRegister404Routes: true})`.
108131
func (g *Group) Group(prefix string, middleware ...MiddlewareFunc) (sg *Group) {
109132
m := make([]MiddlewareFunc, 0, len(g.middleware)+len(middleware))
110133
m = append(m, g.middleware...)

group_test.go

Lines changed: 55 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"github.com/stretchr/testify/assert"
1515
)
1616

17-
func TestGroup_withoutRouteWillNotExecuteMiddleware(t *testing.T) {
17+
func TestGroup_withoutRouteWillExecuteMiddleware(t *testing.T) {
1818
e := New()
1919

2020
called := false
@@ -24,7 +24,29 @@ func TestGroup_withoutRouteWillNotExecuteMiddleware(t *testing.T) {
2424
return c.NoContent(http.StatusTeapot)
2525
}
2626
}
27-
// even though group has middleware it will not be executed when there are no routes under that group
27+
// even though group has middleware it will be executed when there are no routes under that group
28+
// because implicit routes ("" and "/*") are created for the group
29+
_ = e.Group("/group", mw)
30+
31+
status, body := request(http.MethodGet, "/group/nope", e)
32+
assert.Equal(t, http.StatusTeapot, status)
33+
assert.Equal(t, "", body)
34+
35+
assert.True(t, called)
36+
}
37+
38+
func TestGroup_withoutRouteWillNotExecuteMiddleware(t *testing.T) {
39+
e := NewWithConfig(Config{NoGroupAutoRegister404Routes: true})
40+
41+
called := false
42+
mw := func(next HandlerFunc) HandlerFunc {
43+
return func(c *Context) error {
44+
called = true
45+
return c.NoContent(http.StatusTeapot)
46+
}
47+
}
48+
// even though group has middleware it will be executed when there are no routes under that group
49+
// because implicit routes ("" and "/*") are created for the group
2850
_ = e.Group("/group", mw)
2951

3052
status, body := request(http.MethodGet, "/group/nope", e)
@@ -34,7 +56,7 @@ func TestGroup_withoutRouteWillNotExecuteMiddleware(t *testing.T) {
3456
assert.False(t, called)
3557
}
3658

37-
func TestGroup_withRoutesWillNotExecuteMiddlewareFor404(t *testing.T) {
59+
func TestGroup_withRoutesWillExecuteMiddlewareFor404(t *testing.T) {
3860
e := New()
3961

4062
called := false
@@ -45,15 +67,17 @@ func TestGroup_withRoutesWillNotExecuteMiddlewareFor404(t *testing.T) {
4567
}
4668
}
4769
// even though group has middleware and routes when we have no match on some route the middlewares for that
48-
// group will not be executed
70+
// group will be executed
4971
g := e.Group("/group", mw)
5072
g.GET("/yes", handlerFunc)
5173

74+
// route was `/group/yes` but we are requesting `/group/nope` which will result 404 by Router, but middleware will be
75+
// not reach the handler and return 418
5276
status, body := request(http.MethodGet, "/group/nope", e)
53-
assert.Equal(t, http.StatusNotFound, status)
54-
assert.Equal(t, `{"message":"Not Found"}`+"\n", body)
77+
assert.Equal(t, http.StatusTeapot, status)
78+
assert.Equal(t, "", body)
5579

56-
assert.False(t, called)
80+
assert.True(t, called)
5781
}
5882

5983
func TestGroup_multiLevelGroup(t *testing.T) {
@@ -425,7 +449,9 @@ func TestGroup_Match(t *testing.T) {
425449
}
426450

427451
func TestGroup_MatchWithErrors(t *testing.T) {
428-
e := New()
452+
e := NewWithConfig(Config{
453+
Router: NewRouter(RouterConfig{AllowOverwritingRoute: false}), // to trigger "duplicate route" error
454+
})
429455

430456
users := e.Group("/users")
431457
users.GET("/activate", func(c *Context) error {
@@ -770,25 +796,25 @@ func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) {
770796
name: "ok, custom 404 handler is called with middleware",
771797
givenCustom404: true,
772798
whenURL: "/group/test3",
773-
expectBody: "404 GET /group/*",
799+
expectBody: "404 (local) GET /group/*",
774800
expectCode: http.StatusNotFound,
775801
expectMiddlewareCalled: true, // because RouteNotFound is added after middleware is added
776802
},
777803
{
778-
name: "ok, default group 404 handler is not called with middleware",
804+
name: "ok, default group 404 handler is called with middleware",
779805
givenCustom404: false,
780806
whenURL: "/group/test3",
781-
expectBody: "404 GET /*",
807+
expectBody: "404 (global) GET /group/*",
782808
expectCode: http.StatusNotFound,
783-
expectMiddlewareCalled: false, // because RouteNotFound is added before middleware is added
809+
expectMiddlewareCalled: true, // because RouteNotFound is added before middleware is added
784810
},
785811
{
786812
name: "ok, (no slash) default group 404 handler is called with middleware",
787813
givenCustom404: false,
788814
whenURL: "/group",
789-
expectBody: "404 GET /*",
815+
expectBody: "404 (global) GET /group",
790816
expectCode: http.StatusNotFound,
791-
expectMiddlewareCalled: false, // because RouteNotFound is added before middleware is added
817+
expectMiddlewareCalled: true, // because RouteNotFound is added before middleware is added
792818
},
793819
}
794820
for _, tc := range testCases {
@@ -797,13 +823,23 @@ func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) {
797823
okHandler := func(c *Context) error {
798824
return c.String(http.StatusOK, c.Request().Method+" "+c.Path())
799825
}
800-
notFoundHandler := func(c *Context) error {
801-
return c.String(http.StatusNotFound, "404 "+c.Request().Method+" "+c.Path())
826+
old404 := notFoundHandler
827+
defer func() { notFoundHandler = old404 }()
828+
829+
localNotFoundHandler := func(c *Context) error {
830+
return c.String(http.StatusNotFound, "404 (local) "+c.Request().Method+" "+c.Path())
802831
}
803832

804-
e := New()
833+
e := NewWithConfig(Config{
834+
Router: NewRouter(RouterConfig{
835+
AllowOverwritingRoute: true,
836+
NotFoundHandler: func(c *Context) error {
837+
return c.String(http.StatusNotFound, "404 (global) "+c.Request().Method+" "+c.Path())
838+
},
839+
}),
840+
})
805841
e.GET("/test1", okHandler)
806-
e.RouteNotFound("/*", notFoundHandler)
842+
e.RouteNotFound("/*", localNotFoundHandler)
807843

808844
g := e.Group("/group")
809845
g.GET("/test1", okHandler)
@@ -816,7 +852,7 @@ func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) {
816852
}
817853
})
818854
if tc.givenCustom404 {
819-
g.RouteNotFound("/*", notFoundHandler)
855+
g.RouteNotFound("/*", localNotFoundHandler)
820856
}
821857

822858
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)

route.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ import (
1212
)
1313

1414
// Route contains information to adding/registering new route with the router.
15-
// Method+Path pair uniquely identifies the Route. It is mandatory to provide Method+Path+Handler fields.
15+
// Method+Path pair uniquely identifies the Route. It is mandatory to provide Method+Path fields.
1616
type Route struct {
17-
Method string
18-
Path string
19-
Name string
17+
Method string
18+
Path string
19+
Name string
20+
21+
// HandlerFunc is a function that handles HTTP requests. This could be left nil when the Router implementation allows
22+
// fallback to default/global handlers in certain situations.
2023
Handler HandlerFunc
2124
Middlewares []MiddlewareFunc
2225
}

router.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,27 @@ type DefaultRouter struct {
7474

7575
// RouterConfig is configuration options for (default) router
7676
type RouterConfig struct {
77-
NotFoundHandler HandlerFunc
78-
MethodNotAllowedHandler HandlerFunc
79-
OptionsMethodHandler HandlerFunc
80-
AllowOverwritingRoute bool
81-
UnescapePathParamValues bool
77+
// NotFoundHandler is a handler that is executed when no route matches the request.
78+
NotFoundHandler HandlerFunc
79+
80+
// MethodNotAllowedHandler is a handler that is executed when no route with exact METHOD matches the request but
81+
// there is a route with same path but different method.
82+
MethodNotAllowedHandler HandlerFunc
83+
84+
// OptionsMethodHandler is a handler that is executed when an OPTIONS request is made.
85+
OptionsMethodHandler HandlerFunc
86+
87+
// AllowOverwritingRoute allows overwriting existing routes. If false, then adding a route with the same method
88+
// and path will return an error.
89+
AllowOverwritingRoute bool
90+
91+
// UnescapePathParamValues forces router to unescape path parameter values before setting them in context.
92+
UnescapePathParamValues bool
93+
94+
// UseEscapedPathForMatching forces router to use an escaped path (req.URL.RawPath instead of req.URL.Path) for matching.
95+
// Difference between URL.RawPath and URL.Path is:
96+
// * URL.Path is where request path is stored. Value is stored in decoded form: /%47%6f%2f becomes /Go/.
97+
// * URL.RawPath is an optional field which only gets set if the default encoding is different from Path.
8298
UseEscapedPathForMatching bool
8399

84100
// AutoHandleHEAD enables automatic handling of HTTP HEAD requests by
@@ -491,8 +507,16 @@ func newAddRouteError(route Route, err error) *AddRouteError {
491507
// Add registers a new route for method and path with matching handler.
492508
func (r *DefaultRouter) Add(route Route) (RouteInfo, error) {
493509
if route.Handler == nil {
494-
return RouteInfo{}, newAddRouteError(route, errors.New("adding route without handler function"))
510+
switch route.Method {
511+
case RouteNotFound:
512+
route.Handler = r.notFoundHandler
513+
case http.MethodOptions:
514+
route.Handler = r.optionsMethodHandler
515+
default:
516+
return RouteInfo{}, newAddRouteError(route, errors.New("adding route without handler function"))
517+
}
495518
}
519+
496520
method := route.Method
497521
path := normalizePathSlash(route.Path)
498522

0 commit comments

Comments
 (0)