Skip to content

Commit 36e0aaa

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 d17c907 commit 36e0aaa

6 files changed

Lines changed: 113 additions & 36 deletions

File tree

echo.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ type Echo struct {
100100

101101
// formParseMaxMemory is passed to Context for multipart form parsing (See http.Request.ParseMultipartForm)
102102
formParseMaxMemory int64
103+
104+
// noGroupAutoRegisterRoutes is a flag that indicates whether echo.Group should NOT register 404 routes automatically
105+
// when there are middlewares registered with the group.
106+
noGroupAutoRegisterRoutes bool
103107
}
104108

105109
// JSONSerializer is the interface that encodes and decodes JSON to and from interfaces.
@@ -288,6 +292,10 @@ type Config struct {
288292
// FormParseMaxMemory is default value for memory limit that is used
289293
// when parsing multipart forms (See (*http.Request).ParseMultipartForm)
290294
FormParseMaxMemory int64
295+
296+
// NoGroupAutoRegister404Routes bool is a flag that indicates whether echo.Group should NOT register 404 routes automatically
297+
// when there are middlewares registered with the group.
298+
NoGroupAutoRegister404Routes bool
291299
}
292300

293301
// NewWithConfig creates an instance of Echo with given configuration.
@@ -326,6 +334,8 @@ func NewWithConfig(config Config) *Echo {
326334
if config.FormParseMaxMemory > 0 {
327335
e.formParseMaxMemory = config.FormParseMaxMemory
328336
}
337+
e.noGroupAutoRegisterRoutes = config.NoGroupAutoRegister404Routes
338+
329339
return e
330340
}
331341

@@ -342,7 +352,9 @@ func New() *Echo {
342352
}
343353

344354
e.serveHTTPFunc = e.serveHTTP
345-
e.router = NewRouter(RouterConfig{})
355+
e.router = NewRouter(RouterConfig{
356+
AllowOverwritingRoute: true,
357+
})
346358
e.HTTPErrorHandler = DefaultHTTPErrorHandler(false)
347359
e.contextPool.New = func() any {
348360
return newContext(nil, nil, e)
@@ -657,7 +669,11 @@ func (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...Middl
657669

658670
// Group creates a new router group with prefix and optional group-level middleware.
659671
func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) {
660-
g = &Group{prefix: prefix, echo: e}
672+
g = &Group{
673+
echo: e,
674+
prefix: prefix,
675+
noAutoRegisterRoutes: e.noGroupAutoRegisterRoutes,
676+
}
661677
g.Use(m...)
662678
return
663679
}

echo_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -824,12 +824,12 @@ func TestEchoServeHTTPPathEncoding(t *testing.T) {
824824
func TestEchoGroup(t *testing.T) {
825825
e := New()
826826
buf := new(bytes.Buffer)
827-
e.Use(MiddlewareFunc(func(next HandlerFunc) HandlerFunc {
827+
e.Use(func(next HandlerFunc) HandlerFunc {
828828
return func(c *Context) error {
829829
buf.WriteString("0")
830830
return next(c)
831831
}
832-
}))
832+
})
833833
h := func(c *Context) error {
834834
return c.NoContent(http.StatusOK)
835835
}

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: 37 additions & 22 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,17 +24,18 @@ 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
2829
_ = e.Group("/group", mw)
2930

3031
status, body := request(http.MethodGet, "/group/nope", e)
31-
assert.Equal(t, http.StatusNotFound, status)
32-
assert.Equal(t, `{"message":"Not Found"}`+"\n", body)
32+
assert.Equal(t, http.StatusTeapot, status)
33+
assert.Equal(t, "", body)
3334

34-
assert.False(t, called)
35+
assert.True(t, called)
3536
}
3637

37-
func TestGroup_withRoutesWillNotExecuteMiddlewareFor404(t *testing.T) {
38+
func TestGroup_withRoutesWillExecuteMiddlewareFor404(t *testing.T) {
3839
e := New()
3940

4041
called := false
@@ -45,15 +46,17 @@ func TestGroup_withRoutesWillNotExecuteMiddlewareFor404(t *testing.T) {
4546
}
4647
}
4748
// 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
49+
// group will be executed
4950
g := e.Group("/group", mw)
5051
g.GET("/yes", handlerFunc)
5152

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

56-
assert.False(t, called)
59+
assert.True(t, called)
5760
}
5861

5962
func TestGroup_multiLevelGroup(t *testing.T) {
@@ -407,7 +410,9 @@ func TestGroup_Match(t *testing.T) {
407410
}
408411

409412
func TestGroup_MatchWithErrors(t *testing.T) {
410-
e := New()
413+
e := NewWithConfig(Config{
414+
Router: NewRouter(RouterConfig{AllowOverwritingRoute: false}), // to trigger "duplicate route" error
415+
})
411416

412417
users := e.Group("/users")
413418
users.GET("/activate", func(c *Context) error {
@@ -752,25 +757,25 @@ func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) {
752757
name: "ok, custom 404 handler is called with middleware",
753758
givenCustom404: true,
754759
whenURL: "/group/test3",
755-
expectBody: "404 GET /group/*",
760+
expectBody: "404 (local) GET /group/*",
756761
expectCode: http.StatusNotFound,
757762
expectMiddlewareCalled: true, // because RouteNotFound is added after middleware is added
758763
},
759764
{
760-
name: "ok, default group 404 handler is not called with middleware",
765+
name: "ok, default group 404 handler is called with middleware",
761766
givenCustom404: false,
762767
whenURL: "/group/test3",
763-
expectBody: "404 GET /*",
768+
expectBody: "404 (global) GET /group/*",
764769
expectCode: http.StatusNotFound,
765-
expectMiddlewareCalled: false, // because RouteNotFound is added before middleware is added
770+
expectMiddlewareCalled: true, // because RouteNotFound is added before middleware is added
766771
},
767772
{
768773
name: "ok, (no slash) default group 404 handler is called with middleware",
769774
givenCustom404: false,
770775
whenURL: "/group",
771-
expectBody: "404 GET /*",
776+
expectBody: "404 (global) GET /group",
772777
expectCode: http.StatusNotFound,
773-
expectMiddlewareCalled: false, // because RouteNotFound is added before middleware is added
778+
expectMiddlewareCalled: true, // because RouteNotFound is added before middleware is added
774779
},
775780
}
776781
for _, tc := range testCases {
@@ -779,13 +784,23 @@ func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) {
779784
okHandler := func(c *Context) error {
780785
return c.String(http.StatusOK, c.Request().Method+" "+c.Path())
781786
}
782-
notFoundHandler := func(c *Context) error {
783-
return c.String(http.StatusNotFound, "404 "+c.Request().Method+" "+c.Path())
787+
old404 := notFoundHandler
788+
defer func() { notFoundHandler = old404 }()
789+
790+
localNotFoundHandler := func(c *Context) error {
791+
return c.String(http.StatusNotFound, "404 (local) "+c.Request().Method+" "+c.Path())
784792
}
785793

786-
e := New()
794+
e := NewWithConfig(Config{
795+
Router: NewRouter(RouterConfig{
796+
AllowOverwritingRoute: true,
797+
NotFoundHandler: func(c *Context) error {
798+
return c.String(http.StatusNotFound, "404 (global) "+c.Request().Method+" "+c.Path())
799+
},
800+
}),
801+
})
787802
e.GET("/test1", okHandler)
788-
e.RouteNotFound("/*", notFoundHandler)
803+
e.RouteNotFound("/*", localNotFoundHandler)
789804

790805
g := e.Group("/group")
791806
g.GET("/test1", okHandler)
@@ -798,7 +813,7 @@ func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) {
798813
}
799814
})
800815
if tc.givenCustom404 {
801-
g.RouteNotFound("/*", notFoundHandler)
816+
g.RouteNotFound("/*", localNotFoundHandler)
802817
}
803818

804819
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

@@ -446,8 +462,16 @@ func newAddRouteError(route Route, err error) *AddRouteError {
446462
// Add registers a new route for method and path with matching handler.
447463
func (r *DefaultRouter) Add(route Route) (RouteInfo, error) {
448464
if route.Handler == nil {
449-
return RouteInfo{}, newAddRouteError(route, errors.New("adding route without handler function"))
465+
switch route.Method {
466+
case RouteNotFound:
467+
route.Handler = r.notFoundHandler
468+
case http.MethodOptions:
469+
route.Handler = r.optionsMethodHandler
470+
default:
471+
return RouteInfo{}, newAddRouteError(route, errors.New("adding route without handler function"))
472+
}
450473
}
474+
451475
method := route.Method
452476
path := normalizePathSlash(route.Path)
453477

0 commit comments

Comments
 (0)