Skip to content
Open
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
22 changes: 20 additions & 2 deletions echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ type Echo struct {

// formParseMaxMemory is passed to Context for multipart form parsing (See http.Request.ParseMultipartForm)
formParseMaxMemory int64

// noGroupAutoRegisterRoutes is a flag that indicates whether echo.Group should NOT register 404 routes automatically
// when there are middlewares registered with the group.
noGroupAutoRegisterRoutes bool
}

// JSONSerializer is the interface that encodes and decodes JSON to and from interfaces.
Expand Down Expand Up @@ -288,6 +292,12 @@ type Config struct {
// FormParseMaxMemory is default value for memory limit that is used
// when parsing multipart forms (See (*http.Request).ParseMultipartForm)
FormParseMaxMemory int64

// NoGroupAutoRegister404Routes bool is a flag that indicates whether echo.Group should NOT register 404 routes automatically
// when there are middlewares registered with the group.
// Note: if you decide not to register 404 routes automatically, make sure to check if all your middlewares are executed
// as expected. For example - CORS middleware.
NoGroupAutoRegister404Routes bool
}

// NewWithConfig creates an instance of Echo with given configuration.
Expand Down Expand Up @@ -326,6 +336,8 @@ func NewWithConfig(config Config) *Echo {
if config.FormParseMaxMemory > 0 {
e.formParseMaxMemory = config.FormParseMaxMemory
}
e.noGroupAutoRegisterRoutes = config.NoGroupAutoRegister404Routes

return e
}

Expand All @@ -342,7 +354,9 @@ func New() *Echo {
}

e.serveHTTPFunc = e.serveHTTP
e.router = NewRouter(RouterConfig{})
e.router = NewRouter(RouterConfig{
AllowOverwritingRoute: true,
})
e.HTTPErrorHandler = DefaultHTTPErrorHandler(false)
e.contextPool.New = func() any {
return newContext(nil, nil, e)
Expand Down Expand Up @@ -657,7 +671,11 @@ func (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...Middl

// Group creates a new router group with prefix and optional group-level middleware.
func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) {
g = &Group{prefix: prefix, echo: e}
g = &Group{
echo: e,
prefix: prefix,
noAutoRegisterRoutes: e.noGroupAutoRegisterRoutes,
}
g.Use(m...)
return
}
Expand Down
4 changes: 2 additions & 2 deletions echo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -824,12 +824,12 @@ func TestEchoServeHTTPPathEncoding(t *testing.T) {
func TestEchoGroup(t *testing.T) {
e := New()
buf := new(bytes.Buffer)
e.Use(MiddlewareFunc(func(next HandlerFunc) HandlerFunc {
e.Use(func(next HandlerFunc) HandlerFunc {
return func(c *Context) error {
buf.WriteString("0")
return next(c)
}
}))
})
h := func(c *Context) error {
return c.NoContent(http.StatusOK)
}
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ go 1.25.0

require (
github.com/stretchr/testify v1.11.1
golang.org/x/net v0.49.0
golang.org/x/time v0.14.0
golang.org/x/net v0.55.0
golang.org/x/time v0.15.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/text v0.38.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
19 changes: 19 additions & 0 deletions group.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,31 @@ type Group struct {
echo *Echo
prefix string
middleware []MiddlewareFunc

// noAutoRegisterRoutes is a flag that indicates whether Group should NOT register 404 routes automatically
// when there are middlewares registered with the group.
// Note: if you decide not to register 404 routes automatically, make sure to check if all your middlewares are executed
// as expected. For example - CORS middleware.
noAutoRegisterRoutes bool
}

// Use implements `Echo#Use()` for sub-routes within the Group.
// Group middlewares are not executed on request when there is no matching route found.
func (g *Group) Use(middleware ...MiddlewareFunc) {
g.middleware = append(g.middleware, middleware...)
if len(g.middleware) == 0 {
return
}
if g.noAutoRegisterRoutes {
return
}
// group level middlewares are different from Echo `Pre` and `Use` middlewares (those are global). Group level middlewares
// 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.
// Note: we use nil handler so Router would choose the default 404 handler. This may not work with custom routers.
g.RouteNotFound("", nil)
g.RouteNotFound("/*", nil)
}

// CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. Panics on error.
Expand Down
74 changes: 55 additions & 19 deletions group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/assert"
)

func TestGroup_withoutRouteWillNotExecuteMiddleware(t *testing.T) {
func TestGroup_withoutRouteWillExecuteMiddleware(t *testing.T) {
e := New()

called := false
Expand All @@ -24,7 +24,29 @@ func TestGroup_withoutRouteWillNotExecuteMiddleware(t *testing.T) {
return c.NoContent(http.StatusTeapot)
}
}
// even though group has middleware it will not be executed when there are no routes under that group
// even though group has middleware it will be executed when there are no routes under that group
// because implicit routes ("" and "/*") are created for the group
_ = e.Group("/group", mw)

status, body := request(http.MethodGet, "/group/nope", e)
assert.Equal(t, http.StatusTeapot, status)
assert.Equal(t, "", body)

assert.True(t, called)
}

func TestGroup_withoutRouteWillNotExecuteMiddleware(t *testing.T) {
e := NewWithConfig(Config{NoGroupAutoRegister404Routes: true})

called := false
mw := func(next HandlerFunc) HandlerFunc {
return func(c *Context) error {
called = true
return c.NoContent(http.StatusTeapot)
}
}
// even though group has middleware it will be executed when there are no routes under that group
// because implicit routes ("" and "/*") are created for the group
_ = e.Group("/group", mw)

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

func TestGroup_withRoutesWillNotExecuteMiddlewareFor404(t *testing.T) {
func TestGroup_withRoutesWillExecuteMiddlewareFor404(t *testing.T) {
e := New()

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

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

assert.False(t, called)
assert.True(t, called)
}

func TestGroup_multiLevelGroup(t *testing.T) {
Expand Down Expand Up @@ -407,7 +431,9 @@ func TestGroup_Match(t *testing.T) {
}

func TestGroup_MatchWithErrors(t *testing.T) {
e := New()
e := NewWithConfig(Config{
Router: NewRouter(RouterConfig{AllowOverwritingRoute: false}), // to trigger "duplicate route" error
})

users := e.Group("/users")
users.GET("/activate", func(c *Context) error {
Expand Down Expand Up @@ -752,25 +778,25 @@ func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) {
name: "ok, custom 404 handler is called with middleware",
givenCustom404: true,
whenURL: "/group/test3",
expectBody: "404 GET /group/*",
expectBody: "404 (local) GET /group/*",
expectCode: http.StatusNotFound,
expectMiddlewareCalled: true, // because RouteNotFound is added after middleware is added
},
{
name: "ok, default group 404 handler is not called with middleware",
name: "ok, default group 404 handler is called with middleware",
givenCustom404: false,
whenURL: "/group/test3",
expectBody: "404 GET /*",
expectBody: "404 (global) GET /group/*",
expectCode: http.StatusNotFound,
expectMiddlewareCalled: false, // because RouteNotFound is added before middleware is added
expectMiddlewareCalled: true, // because RouteNotFound is added before middleware is added
},
{
name: "ok, (no slash) default group 404 handler is called with middleware",
givenCustom404: false,
whenURL: "/group",
expectBody: "404 GET /*",
expectBody: "404 (global) GET /group",
expectCode: http.StatusNotFound,
expectMiddlewareCalled: false, // because RouteNotFound is added before middleware is added
expectMiddlewareCalled: true, // because RouteNotFound is added before middleware is added
},
}
for _, tc := range testCases {
Expand All @@ -779,13 +805,23 @@ func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) {
okHandler := func(c *Context) error {
return c.String(http.StatusOK, c.Request().Method+" "+c.Path())
}
notFoundHandler := func(c *Context) error {
return c.String(http.StatusNotFound, "404 "+c.Request().Method+" "+c.Path())
old404 := notFoundHandler
defer func() { notFoundHandler = old404 }()

localNotFoundHandler := func(c *Context) error {
return c.String(http.StatusNotFound, "404 (local) "+c.Request().Method+" "+c.Path())
}

e := New()
e := NewWithConfig(Config{
Router: NewRouter(RouterConfig{
AllowOverwritingRoute: true,
NotFoundHandler: func(c *Context) error {
return c.String(http.StatusNotFound, "404 (global) "+c.Request().Method+" "+c.Path())
},
}),
})
e.GET("/test1", okHandler)
e.RouteNotFound("/*", notFoundHandler)
e.RouteNotFound("/*", localNotFoundHandler)

g := e.Group("/group")
g.GET("/test1", okHandler)
Expand All @@ -798,7 +834,7 @@ func TestGroup_RouteNotFoundWithMiddleware(t *testing.T) {
}
})
if tc.givenCustom404 {
g.RouteNotFound("/*", notFoundHandler)
g.RouteNotFound("/*", localNotFoundHandler)
}

req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
Expand Down
11 changes: 7 additions & 4 deletions route.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import (
)

// Route contains information to adding/registering new route with the router.
// Method+Path pair uniquely identifies the Route. It is mandatory to provide Method+Path+Handler fields.
// Method+Path pair uniquely identifies the Route. It is mandatory to provide Method+Path fields.
type Route struct {
Method string
Path string
Name string
Method string
Path string
Name string

// HandlerFunc is a function that handles HTTP requests. This could be left nil when the Router implementation allows
// fallback to default/global handlers in certain situations.
Handler HandlerFunc
Middlewares []MiddlewareFunc
}
Expand Down
36 changes: 30 additions & 6 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,27 @@ type DefaultRouter struct {

// RouterConfig is configuration options for (default) router
type RouterConfig struct {
NotFoundHandler HandlerFunc
MethodNotAllowedHandler HandlerFunc
OptionsMethodHandler HandlerFunc
AllowOverwritingRoute bool
UnescapePathParamValues bool
// NotFoundHandler is a handler that is executed when no route matches the request.
NotFoundHandler HandlerFunc

// MethodNotAllowedHandler is a handler that is executed when no route with exact METHOD matches the request but
// there is a route with same path but different method.
MethodNotAllowedHandler HandlerFunc

// OptionsMethodHandler is a handler that is executed when an OPTIONS request is made.
OptionsMethodHandler HandlerFunc

// AllowOverwritingRoute allows overwriting existing routes. If false, then adding a route with the same method
// and path will return an error.
AllowOverwritingRoute bool

// UnescapePathParamValues forces router to unescape path parameter values before setting them in context.
UnescapePathParamValues bool

// UseEscapedPathForMatching forces router to use an escaped path (req.URL.RawPath instead of req.URL.Path) for matching.
// Difference between URL.RawPath and URL.Path is:
// * URL.Path is where request path is stored. Value is stored in decoded form: /%47%6f%2f becomes /Go/.
// * URL.RawPath is an optional field which only gets set if the default encoding is different from Path.
UseEscapedPathForMatching bool
}

Expand Down Expand Up @@ -446,8 +462,16 @@ func newAddRouteError(route Route, err error) *AddRouteError {
// Add registers a new route for method and path with matching handler.
func (r *DefaultRouter) Add(route Route) (RouteInfo, error) {
if route.Handler == nil {
return RouteInfo{}, newAddRouteError(route, errors.New("adding route without handler function"))
switch route.Method {
case RouteNotFound:
route.Handler = r.notFoundHandler
case http.MethodOptions:
route.Handler = r.optionsMethodHandler
default:
return RouteInfo{}, newAddRouteError(route, errors.New("adding route without handler function"))
}
}

method := route.Method
path := normalizePathSlash(route.Path)

Expand Down
Loading