Skip to content

Commit 9f5d08b

Browse files
committed
Add support for registering handlers for 404 routes
1 parent ddb66e1 commit 9f5d08b

File tree

6 files changed

+372
-17
lines changed

6 files changed

+372
-17
lines changed

echo.go

+13
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ const (
183183
PROPFIND = "PROPFIND"
184184
// REPORT Method can be used to get information about a resource, see rfc 3253
185185
REPORT = "REPORT"
186+
// RouteNotFound is special method type for routes handling "route not found" (404) cases
187+
RouteNotFound = "echo_route_not_found"
186188
)
187189

188190
// Headers
@@ -480,6 +482,16 @@ func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
480482
return e.Add(http.MethodTrace, path, h, m...)
481483
}
482484

485+
// RouteNotFound registers a special-case route which is executed when no other route is found (i.e. HTTP 404 cases)
486+
// for current request URL.
487+
// Path supports static and named/any parameters just like other http method is defined. Generally path is ended with
488+
// wildcard/match-any character (`/*`, `/download/*` etc).
489+
//
490+
// Example: `e.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })`
491+
func (e *Echo) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
492+
return e.Add(RouteNotFound, path, h, m...)
493+
}
494+
483495
// Any registers a new route for all HTTP methods and path with matching handler
484496
// in the router with optional route-level middleware.
485497
func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
@@ -515,6 +527,7 @@ func (e *Echo) File(path, file string, m ...MiddlewareFunc) *Route {
515527
func (e *Echo) add(host, method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
516528
name := handlerName(handler)
517529
router := e.findRouter(host)
530+
// FIXME: when handler+middleware are both nil ... make it behave like handler removal
518531
router.Add(method, path, func(c Context) error {
519532
h := applyMiddleware(handler, middleware...)
520533
return h(c)

echo_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,70 @@ func TestEchoNotFound(t *testing.T) {
766766
assert.Equal(t, http.StatusNotFound, rec.Code)
767767
}
768768

769+
func TestEcho_RouteNotFound(t *testing.T) {
770+
var testCases = []struct {
771+
name string
772+
whenURL string
773+
expectRoute interface{}
774+
expectCode int
775+
}{
776+
{
777+
name: "404, route to static not found handler /a/c/xx",
778+
whenURL: "/a/c/xx",
779+
expectRoute: "GET /a/c/xx",
780+
expectCode: http.StatusNotFound,
781+
},
782+
{
783+
name: "404, route to path param not found handler /a/:file",
784+
whenURL: "/a/echo.exe",
785+
expectRoute: "GET /a/:file",
786+
expectCode: http.StatusNotFound,
787+
},
788+
{
789+
name: "404, route to any not found handler /*",
790+
whenURL: "/b/echo.exe",
791+
expectRoute: "GET /*",
792+
expectCode: http.StatusNotFound,
793+
},
794+
{
795+
name: "200, route /a/c/df to /a/c/df",
796+
whenURL: "/a/c/df",
797+
expectRoute: "GET /a/c/df",
798+
expectCode: http.StatusOK,
799+
},
800+
}
801+
802+
for _, tc := range testCases {
803+
t.Run(tc.name, func(t *testing.T) {
804+
e := New()
805+
806+
okHandler := func(c Context) error {
807+
return c.String(http.StatusOK, c.Request().Method+" "+c.Path())
808+
}
809+
notFoundHandler := func(c Context) error {
810+
return c.String(http.StatusNotFound, c.Request().Method+" "+c.Path())
811+
}
812+
813+
e.GET("/", okHandler)
814+
e.GET("/a/c/df", okHandler)
815+
e.GET("/a/b*", okHandler)
816+
e.PUT("/*", okHandler)
817+
818+
e.RouteNotFound("/a/c/xx", notFoundHandler) // static
819+
e.RouteNotFound("/a/:file", notFoundHandler) // param
820+
e.RouteNotFound("/*", notFoundHandler) // any
821+
822+
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
823+
rec := httptest.NewRecorder()
824+
825+
e.ServeHTTP(rec, req)
826+
827+
assert.Equal(t, tc.expectCode, rec.Code)
828+
assert.Equal(t, tc.expectRoute, rec.Body.String())
829+
})
830+
}
831+
}
832+
769833
func TestEchoMethodNotAllowed(t *testing.T) {
770834
e := New()
771835

group.go

+7
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ func (g *Group) File(path, file string) {
107107
g.file(path, file, g.GET)
108108
}
109109

110+
// RouteNotFound implements `Echo#RouteNotFound()` for sub-routes within the Group.
111+
//
112+
// Example: `g.RouteNotFound("/*", func(c echo.Context) error { return c.NoContent(http.StatusNotFound) })`
113+
func (g *Group) RouteNotFound(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
114+
return g.Add(RouteNotFound, path, h, m...)
115+
}
116+
110117
// Add implements `Echo#Add()` for sub-routes within the Group.
111118
func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
112119
// Combine into a new slice to avoid accidentally passing the same slice for

group_test.go

+65
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,68 @@ func TestGroupRouteMiddlewareWithMatchAny(t *testing.T) {
119119
assert.Equal(t, "/*", m)
120120

121121
}
122+
123+
func TestGroup_RouteNotFound(t *testing.T) {
124+
var testCases = []struct {
125+
name string
126+
whenURL string
127+
expectRoute interface{}
128+
expectCode int
129+
}{
130+
{
131+
name: "404, route to static not found handler /group/a/c/xx",
132+
whenURL: "/group/a/c/xx",
133+
expectRoute: "GET /group/a/c/xx",
134+
expectCode: http.StatusNotFound,
135+
},
136+
{
137+
name: "404, route to path param not found handler /group/a/:file",
138+
whenURL: "/group/a/echo.exe",
139+
expectRoute: "GET /group/a/:file",
140+
expectCode: http.StatusNotFound,
141+
},
142+
{
143+
name: "404, route to any not found handler /group/*",
144+
whenURL: "/group/b/echo.exe",
145+
expectRoute: "GET /group/*",
146+
expectCode: http.StatusNotFound,
147+
},
148+
{
149+
name: "200, route /group/a/c/df to /group/a/c/df",
150+
whenURL: "/group/a/c/df",
151+
expectRoute: "GET /group/a/c/df",
152+
expectCode: http.StatusOK,
153+
},
154+
}
155+
156+
for _, tc := range testCases {
157+
t.Run(tc.name, func(t *testing.T) {
158+
e := New()
159+
g := e.Group("/group")
160+
161+
okHandler := func(c Context) error {
162+
return c.String(http.StatusOK, c.Request().Method+" "+c.Path())
163+
}
164+
notFoundHandler := func(c Context) error {
165+
return c.String(http.StatusNotFound, c.Request().Method+" "+c.Path())
166+
}
167+
168+
g.GET("/", okHandler)
169+
g.GET("/a/c/df", okHandler)
170+
g.GET("/a/b*", okHandler)
171+
g.PUT("/*", okHandler)
172+
173+
g.RouteNotFound("/a/c/xx", notFoundHandler) // static
174+
g.RouteNotFound("/a/:file", notFoundHandler) // param
175+
g.RouteNotFound("/*", notFoundHandler) // any
176+
177+
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
178+
rec := httptest.NewRecorder()
179+
180+
e.ServeHTTP(rec, req)
181+
182+
assert.Equal(t, tc.expectCode, rec.Code)
183+
assert.Equal(t, tc.expectRoute, rec.Body.String())
184+
})
185+
}
186+
}

router.go

+38-17
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type (
2828
isLeaf bool
2929
// isHandler indicates that node has at least one handler registered to it
3030
isHandler bool
31+
32+
// notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases
33+
notFoundHandler HandlerFunc
3134
}
3235
kind uint8
3336
children []*node
@@ -68,6 +71,7 @@ func (m *methodHandler) isHandler() bool {
6871
m.put != nil ||
6972
m.trace != nil ||
7073
m.report != nil
74+
// RouteNotFound/404 is not considered as a handler
7175
}
7276

7377
func (m *methodHandler) updateAllowHeader() {
@@ -369,6 +373,9 @@ func (n *node) addHandler(method string, h HandlerFunc) {
369373
n.methodHandler.trace = h
370374
case REPORT:
371375
n.methodHandler.report = h
376+
case RouteNotFound:
377+
n.notFoundHandler = h
378+
return // RouteNotFound/404 is not considered as a handler so no further logic needs to be executed
372379
}
373380

374381
n.methodHandler.updateAllowHeader()
@@ -403,7 +410,7 @@ func (n *node) findHandler(method string) HandlerFunc {
403410
return n.methodHandler.trace
404411
case REPORT:
405412
return n.methodHandler.report
406-
default:
413+
default: // RouteNotFound/404 is not considered as a handler
407414
return nil
408415
}
409416
}
@@ -506,7 +513,7 @@ func (r *Router) Find(method, path string, c Context) {
506513
// No matching prefix, let's backtrack to the first possible alternative node of the decision path
507514
nk, ok := backtrackToNextNodeKind(staticKind)
508515
if !ok {
509-
return // No other possibilities on the decision path
516+
return // No other possibilities on the decision path, handler will be whatever context is reset to.
510517
} else if nk == paramKind {
511518
goto Param
512519
// NOTE: this case (backtracking from static node to previous any node) can not happen by current any matching logic. Any node is end of search currently
@@ -522,15 +529,21 @@ func (r *Router) Find(method, path string, c Context) {
522529
search = search[lcpLen:]
523530
searchIndex = searchIndex + lcpLen
524531

525-
// Finish routing if no remaining search and we are on a node with handler and matching method type
526-
if search == "" && currentNode.isHandler {
527-
// check if current node has handler registered for http method we are looking for. we store currentNode as
528-
// best matching in case we do no find no more routes matching this path+method
529-
if previousBestMatchNode == nil {
530-
previousBestMatchNode = currentNode
531-
}
532-
if h := currentNode.findHandler(method); h != nil {
533-
matchedHandler = h
532+
// Finish routing if is no request path remaining to search
533+
if search == "" {
534+
// in case of node that is handler we have exact method type match or something for 405 to use
535+
if currentNode.isHandler {
536+
// check if current node has handler registered for http method we are looking for. we store currentNode as
537+
// best matching in case we do no find no more routes matching this path+method
538+
if previousBestMatchNode == nil {
539+
previousBestMatchNode = currentNode
540+
}
541+
if h := currentNode.findHandler(method); h != nil {
542+
matchedHandler = h
543+
break
544+
}
545+
} else if currentNode.notFoundHandler != nil {
546+
matchedHandler = currentNode.notFoundHandler
534547
break
535548
}
536549
}
@@ -550,7 +563,8 @@ func (r *Router) Find(method, path string, c Context) {
550563
i := 0
551564
l := len(search)
552565
if currentNode.isLeaf {
553-
// when param node does not have any children then param node should act similarly to any node - consider all remaining search as match
566+
// when param node does not have any children (path param is last piece of route path) then param node should
567+
// act similarly to any node - consider all remaining search as match
554568
i = l
555569
} else {
556570
for ; i < l && search[i] != '/'; i++ {
@@ -575,13 +589,16 @@ func (r *Router) Find(method, path string, c Context) {
575589
searchIndex += +len(search)
576590
search = ""
577591

578-
// check if current node has handler registered for http method we are looking for. we store currentNode as
579-
// best matching in case we do no find no more routes matching this path+method
592+
if h := currentNode.findHandler(method); h != nil {
593+
matchedHandler = h
594+
break
595+
}
596+
// we store currentNode as best matching in case we do not find more routes matching this path+method. Needed for 405
580597
if previousBestMatchNode == nil {
581598
previousBestMatchNode = currentNode
582599
}
583-
if h := currentNode.findHandler(method); h != nil {
584-
matchedHandler = h
600+
if currentNode.notFoundHandler != nil {
601+
matchedHandler = currentNode.notFoundHandler
585602
break
586603
}
587604
}
@@ -604,6 +621,8 @@ func (r *Router) Find(method, path string, c Context) {
604621
return // nothing matched at all
605622
}
606623

624+
// matchedHandler could be method+path handler that we matched or notFoundHandler from node with matching path
625+
// user provided not found (404) handler has priority over generic method not found (405) handler or global 404 handler
607626
if matchedHandler != nil {
608627
ctx.handler = matchedHandler
609628
} else {
@@ -612,7 +631,9 @@ func (r *Router) Find(method, path string, c Context) {
612631
currentNode = previousBestMatchNode
613632

614633
ctx.handler = NotFoundHandler
615-
if currentNode.isHandler {
634+
if currentNode.notFoundHandler != nil {
635+
ctx.handler = currentNode.notFoundHandler
636+
} else if currentNode.isHandler {
616637
ctx.Set(ContextKeyHeaderAllow, currentNode.methodHandler.allowHeader)
617638
ctx.handler = MethodNotAllowedHandler
618639
if method == http.MethodOptions {

0 commit comments

Comments
 (0)