Skip to content

Commit acc661a

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 030f1b3 commit acc661a

6 files changed

Lines changed: 133 additions & 33 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,31 @@ 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.
2127
// Group middlewares are not executed on request when there is no matching route found.
2228
func (g *Group) Use(middleware ...MiddlewareFunc) {
2329
g.middleware = append(g.middleware, middleware...)
30+
if len(g.middleware) == 0 {
31+
return
32+
}
33+
if g.noAutoRegisterRoutes {
34+
return
35+
}
36+
// group level middlewares are different from Echo `Pre` and `Use` middlewares (those are global). Group level middlewares
37+
// are only executed if they are added to the Router with route.
38+
// So we register catch all route (404 is a safe way to emulate route match) for this group and now during routing the
39+
// Router would find route to match our request path and therefore guarantee the middleware(s) will get executed.
40+
// Note: we use nil handler so Router would choose the default 404 handler. This may not work with custom routers.
41+
g.RouteNotFound("", nil)
42+
g.RouteNotFound("/*", nil)
2443
}
2544

2645
// CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. Panics on error.

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
@@ -73,11 +73,27 @@ type DefaultRouter struct {
7373

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

@@ -460,8 +476,16 @@ func newAddRouteError(route Route, err error) *AddRouteError {
460476
// Add registers a new route for method and path with matching handler.
461477
func (r *DefaultRouter) Add(route Route) (RouteInfo, error) {
462478
if route.Handler == nil {
463-
return RouteInfo{}, newAddRouteError(route, errors.New("adding route without handler function"))
479+
switch route.Method {
480+
case RouteNotFound:
481+
route.Handler = r.notFoundHandler
482+
case http.MethodOptions:
483+
route.Handler = r.optionsMethodHandler
484+
default:
485+
return RouteInfo{}, newAddRouteError(route, errors.New("adding route without handler function"))
486+
}
464487
}
488+
465489
method := route.Method
466490
path := normalizePathSlash(route.Path)
467491

0 commit comments

Comments
 (0)