Skip to content

Commit 546a1e5

Browse files
committed
router should check if method is suitable for matching route and if not then continue search in tree (fix #1808)
1 parent f98dc97 commit 546a1e5

File tree

3 files changed

+163
-28
lines changed

3 files changed

+163
-28
lines changed

context_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -464,15 +464,17 @@ func TestContextPath(t *testing.T) {
464464
e := New()
465465
r := e.Router()
466466

467-
r.Add(http.MethodGet, "/users/:id", nil)
467+
handler := func(c Context) error { return c.String(http.StatusOK, "OK") }
468+
469+
r.Add(http.MethodGet, "/users/:id", handler)
468470
c := e.NewContext(nil, nil)
469471
r.Find(http.MethodGet, "/users/1", c)
470472

471473
assert := testify.New(t)
472474

473475
assert.Equal("/users/:id", c.Path())
474476

475-
r.Add(http.MethodGet, "/users/:uid/files/:fid", nil)
477+
r.Add(http.MethodGet, "/users/:uid/files/:fid", handler)
476478
c = e.NewContext(nil, nil)
477479
r.Find(http.MethodGet, "/users/1/files/1", c)
478480
assert.Equal("/users/:uid/files/:fid", c.Path())

router.go

Lines changed: 93 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ type (
2323
methodHandler *methodHandler
2424
paramChild *node
2525
anyChild *node
26+
// isLeaf indicates that node does not have child routes
27+
isLeaf bool
28+
// isHandler indicates that node has at least one handler registered to it
29+
isHandler bool
2630
}
2731
kind uint8
2832
children []*node
@@ -50,6 +54,20 @@ const (
5054
anyLabel = byte('*')
5155
)
5256

57+
func (m *methodHandler) isHandler() bool {
58+
return m.connect != nil ||
59+
m.delete != nil ||
60+
m.get != nil ||
61+
m.head != nil ||
62+
m.options != nil ||
63+
m.patch != nil ||
64+
m.post != nil ||
65+
m.propfind != nil ||
66+
m.put != nil ||
67+
m.trace != nil ||
68+
m.report != nil
69+
}
70+
5371
// NewRouter returns a new Router instance.
5472
func NewRouter(e *Echo) *Router {
5573
return &Router{
@@ -73,6 +91,11 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
7391
pnames := []string{} // Param names
7492
ppath := path // Pristine path
7593

94+
if h == nil && r.echo.Logger != nil {
95+
// FIXME: in future we should return error
96+
r.echo.Logger.Errorf("Adding route without handler function: %v:%v", method, path)
97+
}
98+
7699
for i, lcpIndex := 0, len(path); i < lcpIndex; i++ {
77100
if path[i] == ':' {
78101
j := i + 1
@@ -86,6 +109,7 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
86109
i, lcpIndex = j, len(path)
87110

88111
if i == lcpIndex {
112+
// path node is last fragment of route path. ie. `/users/:id`
89113
r.insert(method, path[:i], h, paramKind, ppath, pnames)
90114
} else {
91115
r.insert(method, path[:i], nil, paramKind, "", nil)
@@ -136,6 +160,7 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
136160
currentNode.ppath = ppath
137161
currentNode.pnames = pnames
138162
}
163+
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
139164
} else if lcpLen < prefixLen {
140165
// Split node
141166
n := newNode(
@@ -149,7 +174,6 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
149174
currentNode.paramChild,
150175
currentNode.anyChild,
151176
)
152-
153177
// Update parent path for all children to new node
154178
for _, child := range currentNode.staticChildren {
155179
child.parent = n
@@ -171,6 +195,8 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
171195
currentNode.pnames = nil
172196
currentNode.paramChild = nil
173197
currentNode.anyChild = nil
198+
currentNode.isLeaf = false
199+
currentNode.isHandler = false
174200

175201
// Only Static children could reach here
176202
currentNode.addStaticChild(n)
@@ -188,6 +214,7 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
188214
// Only Static children could reach here
189215
currentNode.addStaticChild(n)
190216
}
217+
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
191218
} else if lcpLen < searchLen {
192219
search = search[lcpLen:]
193220
c := currentNode.findChildWithLabel(search[0])
@@ -207,6 +234,7 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
207234
case anyKind:
208235
currentNode.anyChild = n
209236
}
237+
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
210238
} else {
211239
// Node already exists
212240
if h != nil {
@@ -233,6 +261,8 @@ func newNode(t kind, pre string, p *node, sc children, mh *methodHandler, ppath
233261
methodHandler: mh,
234262
paramChild: paramChildren,
235263
anyChild: anyChildren,
264+
isLeaf: sc == nil && paramChildren == nil && anyChildren == nil,
265+
isHandler: mh.isHandler(),
236266
}
237267
}
238268

@@ -289,6 +319,12 @@ func (n *node) addHandler(method string, h HandlerFunc) {
289319
case REPORT:
290320
n.methodHandler.report = h
291321
}
322+
323+
if h != nil {
324+
n.isHandler = true
325+
} else {
326+
n.isHandler = n.methodHandler.isHandler()
327+
}
292328
}
293329

294330
func (n *node) findHandler(method string) HandlerFunc {
@@ -343,6 +379,8 @@ func (r *Router) Find(method, path string, c Context) {
343379
currentNode := r.tree // Current node as root
344380

345381
var (
382+
previousBestMatchNode *node
383+
matchedHandler HandlerFunc
346384
// search stores the remaining path to check for match. By each iteration we move from start of path to end of the path
347385
// and search value gets shorter and shorter.
348386
search = path
@@ -362,10 +400,11 @@ func (r *Router) Find(method, path string, c Context) {
362400
valid = currentNode != nil
363401

364402
// Next node type by priority
365-
// NOTE: With the current implementation we never backtrack from an `any` route, so `previous.kind` is
366-
// always `static` or `any`
367-
// If this is changed then for any route next kind would be `static` and this statement should be changed
368-
nextNodeKind = previous.kind + 1
403+
if previous.kind == anyKind {
404+
nextNodeKind = staticKind
405+
} else {
406+
nextNodeKind = previous.kind + 1
407+
}
369408

370409
if fromKind == staticKind {
371410
// when backtracking is done from static kind block we did not change search so nothing to restore
@@ -380,6 +419,7 @@ func (r *Router) Find(method, path string, c Context) {
380419
// for param/any node.prefix value is always `:` so we can not deduce searchIndex from that and must use pValue
381420
// for that index as it would also contain part of path we cut off before moving into node we are backtracking from
382421
searchIndex -= len(paramValues[paramIndex])
422+
paramValues[paramIndex] = ""
383423
}
384424
search = path[searchIndex:]
385425
return
@@ -421,17 +461,25 @@ func (r *Router) Find(method, path string, c Context) {
421461
// goto Any
422462
} else {
423463
// Not found (this should never be possible for static node we are looking currently)
424-
return
464+
break
425465
}
426466
}
427467

428468
// The full prefix has matched, remove the prefix from the remaining search
429469
search = search[lcpLen:]
430470
searchIndex = searchIndex + lcpLen
431471

432-
// Finish routing if no remaining search and we are on an leaf node
433-
if search == "" && currentNode.ppath != "" {
434-
break
472+
// Finish routing if no remaining search and we are on a node with handler and matching method type
473+
if search == "" && currentNode.isHandler {
474+
// check if current node has handler registered for http method we are looking for. we store currentNode as
475+
// best matching in case we do no find no more routes matching this path+method
476+
if previousBestMatchNode == nil {
477+
previousBestMatchNode = currentNode
478+
}
479+
if h := currentNode.findHandler(method); h != nil {
480+
matchedHandler = h
481+
break
482+
}
435483
}
436484

437485
// Static node
@@ -446,15 +494,16 @@ func (r *Router) Find(method, path string, c Context) {
446494
// Param node
447495
if child := currentNode.paramChild; search != "" && child != nil {
448496
currentNode = child
449-
// when param node does not have any children then param node should act similarly to any node - consider all remaining search as match
450-
if currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil {
451-
paramValues[paramIndex] = search
452-
break
497+
i := 0
498+
l := len(search)
499+
if currentNode.isLeaf {
500+
// when param node does not have any children then param node should act similarly to any node - consider all remaining search as match
501+
i = l
502+
} else {
503+
for ; i < l && search[i] != '/'; i++ {
504+
}
453505
}
454506

455-
i, l := 0, len(search)
456-
for ; i < l && search[i] != '/'; i++ {
457-
}
458507
paramValues[paramIndex] = search[:i]
459508
paramIndex++
460509
search = search[i:]
@@ -468,29 +517,50 @@ func (r *Router) Find(method, path string, c Context) {
468517
// If any node is found, use remaining path for paramValues
469518
currentNode = child
470519
paramValues[len(currentNode.pnames)-1] = search
471-
break
520+
// update indexes/search in case we need to backtrack when no handler match is found
521+
paramIndex++
522+
searchIndex += +len(search)
523+
search = ""
524+
525+
// check if current node has handler registered for http method we are looking for. we store currentNode as
526+
// best matching in case we do no find no more routes matching this path+method
527+
if previousBestMatchNode == nil {
528+
previousBestMatchNode = currentNode
529+
}
530+
if h := currentNode.findHandler(method); h != nil {
531+
matchedHandler = h
532+
break
533+
}
472534
}
473535

474536
// Let's backtrack to the first possible alternative node of the decision path
475537
nk, ok := backtrackToNextNodeKind(anyKind)
476538
if !ok {
477-
return // No other possibilities on the decision path
539+
break // No other possibilities on the decision path
478540
} else if nk == paramKind {
479541
goto Param
480542
} else if nk == anyKind {
481543
goto Any
482544
} else {
483545
// Not found
484-
return
546+
break
485547
}
486548
}
487549

488-
ctx.handler = currentNode.findHandler(method)
489-
ctx.path = currentNode.ppath
490-
ctx.pnames = currentNode.pnames
550+
if currentNode == nil && previousBestMatchNode == nil {
551+
return // nothing matched at all
552+
}
491553

492-
if ctx.handler == nil {
554+
if matchedHandler != nil {
555+
ctx.handler = matchedHandler
556+
} else {
557+
// use previous match as basis. although we have no matching handler we have path match.
558+
// so we can send http.StatusMethodNotAllowed (405) instead of http.StatusNotFound (404)
559+
currentNode = previousBestMatchNode
493560
ctx.handler = currentNode.checkMethodNotAllowed()
494561
}
562+
ctx.path = currentNode.ppath
563+
ctx.pnames = currentNode.pnames
564+
495565
return
496566
}

router_test.go

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,69 @@ func TestRouterParam(t *testing.T) {
716716
}
717717
}
718718

719+
func TestMethodNotAllowedAndNotFound(t *testing.T) {
720+
e := New()
721+
r := e.router
722+
723+
// Routes
724+
r.Add(http.MethodGet, "/*", handlerFunc)
725+
r.Add(http.MethodPost, "/users/:id", handlerFunc)
726+
727+
var testCases = []struct {
728+
name string
729+
whenMethod string
730+
whenURL string
731+
expectRoute interface{}
732+
expectParam map[string]string
733+
expectError error
734+
}{
735+
{
736+
name: "exact match for route+method",
737+
whenMethod: http.MethodPost,
738+
whenURL: "/users/1",
739+
expectRoute: "/users/:id",
740+
expectParam: map[string]string{"id": "1"},
741+
},
742+
{
743+
name: "matches node but not method. sends 405 from best match node",
744+
whenMethod: http.MethodPut,
745+
whenURL: "/users/1",
746+
expectRoute: nil,
747+
expectError: ErrMethodNotAllowed,
748+
},
749+
{
750+
name: "best match is any route up in tree",
751+
whenMethod: http.MethodGet,
752+
whenURL: "/users/1",
753+
expectRoute: "/*",
754+
expectParam: map[string]string{"*": "users/1"},
755+
},
756+
}
757+
for _, tc := range testCases {
758+
t.Run(tc.name, func(t *testing.T) {
759+
c := e.NewContext(nil, nil).(*context)
760+
761+
method := http.MethodGet
762+
if tc.whenMethod != "" {
763+
method = tc.whenMethod
764+
}
765+
r.Find(method, tc.whenURL, c)
766+
err := c.handler(c)
767+
768+
if tc.expectError != nil {
769+
assert.Equal(t, tc.expectError, err)
770+
} else {
771+
assert.NoError(t, err)
772+
}
773+
assert.Equal(t, tc.expectRoute, c.Get("path"))
774+
for param, expectedValue := range tc.expectParam {
775+
assert.Equal(t, expectedValue, c.Param(param))
776+
}
777+
checkUnusedParamValues(t, c, tc.expectParam)
778+
})
779+
}
780+
}
781+
719782
func TestRouterTwoParam(t *testing.T) {
720783
e := New()
721784
r := e.router
@@ -2004,10 +2067,10 @@ func TestRouterParam1466(t *testing.T) {
20042067
expectRoute: "/users/:username",
20052068
expectParam: map[string]string{"username": "sharewithme"},
20062069
},
2007-
{
2070+
{ // route `/users/signup` is registered for POST. so param route `/users/:username` (lesser priority) is matched as it has GET handler
20082071
whenURL: "/users/signup",
2009-
expectRoute: nil, // method not found as this route is for POST but request is for GET
2010-
expectParam: map[string]string{"username": ""},
2072+
expectRoute: "/users/:username",
2073+
expectParam: map[string]string{"username": "signup"},
20112074
},
20122075
// Additional assertions for #1479
20132076
{

0 commit comments

Comments
 (0)