mirror of
https://github.com/labstack/echo.git
synced 2025-06-15 00:14:57 +02:00
Poc router stack backtracking (#1791)
Router: PoC stack based backtracking Co-authored-by: stffabi <stffabi@users.noreply.github.com>
This commit is contained in:
194
router.go
194
router.go
@ -2,7 +2,6 @@ package echo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@ -334,21 +333,48 @@ func (r *Router) Find(method, path string, c Context) {
|
|||||||
cn := r.tree // Current node as root
|
cn := r.tree // Current node as root
|
||||||
|
|
||||||
var (
|
var (
|
||||||
search = path
|
search = path
|
||||||
child *node // Child node
|
searchIndex = 0
|
||||||
n int // Param counter
|
n int // Param counter
|
||||||
nk kind // Next kind
|
pvalues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice
|
||||||
nn *node // Next node
|
|
||||||
ns string // Next search
|
|
||||||
pvalues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Backtracking is needed when a dead end (leaf node) is reached in the router tree.
|
||||||
|
// To backtrack the current node will be changed to the parent node and the next kind for the
|
||||||
|
// router logic will be returned based on fromKind or kind of the dead end node (static > param > any).
|
||||||
|
// For example if there is no static node match we should check parent next sibling by kind (param).
|
||||||
|
// Backtracking itself does not check if there is a next sibling, this is done by the router logic.
|
||||||
|
backtrackToNextNodeKind := func(fromKind kind) (nextNodeKind kind, valid bool) {
|
||||||
|
previous := cn
|
||||||
|
cn = previous.parent
|
||||||
|
valid = cn != nil
|
||||||
|
|
||||||
|
// Next node type by priority
|
||||||
|
// NOTE: With the current implementation we never backtrack from an `any` route, so `previous.kind` is
|
||||||
|
// always `static` or `any`
|
||||||
|
// If this is changed then for any route next kind would be `static` and this statement should be changed
|
||||||
|
nextNodeKind = previous.kind + 1
|
||||||
|
|
||||||
|
if fromKind == skind {
|
||||||
|
// when backtracking is done from static kind block we did not change search so nothing to restore
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore search to value it was before we move to current node we are backtracking from.
|
||||||
|
if previous.kind == skind {
|
||||||
|
searchIndex -= len(previous.prefix)
|
||||||
|
} else {
|
||||||
|
n--
|
||||||
|
// for param/any node.prefix value is always `:` so we can not deduce searchIndex from that and must use pValue
|
||||||
|
// for that index as it would also contain part of path we cut off before moving into node we are backtracking from
|
||||||
|
searchIndex -= len(pvalues[n])
|
||||||
|
}
|
||||||
|
search = path[searchIndex:]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Search order static > param > any
|
// Search order static > param > any
|
||||||
for {
|
for {
|
||||||
if search == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
pl := 0 // Prefix length
|
pl := 0 // Prefix length
|
||||||
l := 0 // LCP length
|
l := 0 // LCP length
|
||||||
|
|
||||||
@ -365,60 +391,42 @@ func (r *Router) Find(method, path string, c Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if l == pl {
|
if l != pl {
|
||||||
// Continue search
|
// No matching prefix, let's backtrack to the first possible alternative node of the decision path
|
||||||
search = search[l:]
|
nk, ok := backtrackToNextNodeKind(skind)
|
||||||
// Finish routing if no remaining search and we are on an leaf node
|
if !ok {
|
||||||
if search == "" && (nn == nil || cn.parent == nil || cn.ppath != "") {
|
return // No other possibilities on the decision path
|
||||||
break
|
} else if nk == pkind {
|
||||||
}
|
goto Param
|
||||||
// Handle special case of trailing slash route with existing any route (see #1526)
|
// 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
|
||||||
if search == "" && path[len(path)-1] == '/' && cn.anyChildren != nil {
|
//} else if nk == akind {
|
||||||
goto Any
|
// goto Any
|
||||||
|
} else {
|
||||||
|
// Not found (this should never be possible for static node we are looking currently)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to go back up the tree on no matching prefix or no remaining search
|
// The full prefix has matched, remove the prefix from the remaining search
|
||||||
if l != pl || search == "" {
|
search = search[l:]
|
||||||
if nn == nil { // Issue #1348
|
searchIndex = searchIndex + l
|
||||||
return // Not found
|
|
||||||
}
|
// Finish routing if no remaining search and we are on an leaf node
|
||||||
cn = nn
|
if search == "" && cn.ppath != "" {
|
||||||
search = ns
|
break
|
||||||
if nk == pkind {
|
|
||||||
goto Param
|
|
||||||
} else if nk == akind {
|
|
||||||
goto Any
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static node
|
// Static node
|
||||||
if child = cn.findStaticChild(search[0]); child != nil {
|
if search != "" {
|
||||||
// Save next
|
if child := cn.findStaticChild(search[0]); child != nil {
|
||||||
if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623
|
cn = child
|
||||||
nk = pkind
|
continue
|
||||||
nn = cn
|
|
||||||
ns = search
|
|
||||||
}
|
}
|
||||||
cn = child
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Param:
|
Param:
|
||||||
// Param node
|
// Param node
|
||||||
if child = cn.paramChildren; child != nil {
|
if child := cn.paramChildren; search != "" && child != nil {
|
||||||
// Issue #378
|
|
||||||
if len(pvalues) == n {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save next
|
|
||||||
if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623
|
|
||||||
nk = akind
|
|
||||||
nn = cn
|
|
||||||
ns = search
|
|
||||||
}
|
|
||||||
|
|
||||||
cn = child
|
cn = child
|
||||||
i, l := 0, len(search)
|
i, l := 0, len(search)
|
||||||
for ; i < l && search[i] != '/'; i++ {
|
for ; i < l && search[i] != '/'; i++ {
|
||||||
@ -426,87 +434,39 @@ func (r *Router) Find(method, path string, c Context) {
|
|||||||
pvalues[n] = search[:i]
|
pvalues[n] = search[:i]
|
||||||
n++
|
n++
|
||||||
search = search[i:]
|
search = search[i:]
|
||||||
|
searchIndex = searchIndex + i
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
Any:
|
Any:
|
||||||
// Any node
|
// Any node
|
||||||
if cn = cn.anyChildren; cn != nil {
|
if child := cn.anyChildren; child != nil {
|
||||||
// If any node is found, use remaining path for pvalues
|
// If any node is found, use remaining path for pvalues
|
||||||
|
cn = child
|
||||||
pvalues[len(cn.pnames)-1] = search
|
pvalues[len(cn.pnames)-1] = search
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// No node found, continue at stored next node
|
// Let's backtrack to the first possible alternative node of the decision path
|
||||||
// or find nearest "any" route
|
nk, ok := backtrackToNextNodeKind(akind)
|
||||||
if nn != nil {
|
if !ok {
|
||||||
// No next node to go down in routing (issue #954)
|
return // No other possibilities on the decision path
|
||||||
// Find nearest "any" route going up the routing tree
|
} else if nk == pkind {
|
||||||
search = ns
|
goto Param
|
||||||
np := nn.parent
|
} else if nk == akind {
|
||||||
// Consider param route one level up only
|
goto Any
|
||||||
if cn = nn.paramChildren; cn != nil {
|
} else {
|
||||||
pos := strings.IndexByte(ns, '/')
|
// Not found
|
||||||
if pos == -1 {
|
return
|
||||||
// If no slash is remaining in search string set param value
|
|
||||||
if len(cn.pnames) > 0 {
|
|
||||||
pvalues[len(cn.pnames)-1] = search
|
|
||||||
}
|
|
||||||
break
|
|
||||||
} else if pos > 0 {
|
|
||||||
// Otherwise continue route processing with restored next node
|
|
||||||
cn = nn
|
|
||||||
nn = nil
|
|
||||||
ns = ""
|
|
||||||
goto Param
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// No param route found, try to resolve nearest any route
|
|
||||||
for {
|
|
||||||
np = nn.parent
|
|
||||||
if cn = nn.anyChildren; cn != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if np == nil {
|
|
||||||
break // no further parent nodes in tree, abort
|
|
||||||
}
|
|
||||||
var str strings.Builder
|
|
||||||
str.WriteString(nn.prefix)
|
|
||||||
str.WriteString(search)
|
|
||||||
search = str.String()
|
|
||||||
nn = np
|
|
||||||
}
|
|
||||||
if cn != nil { // use the found "any" route and update path
|
|
||||||
pvalues[len(cn.pnames)-1] = search
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return // Not found
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.handler = cn.findHandler(method)
|
ctx.handler = cn.findHandler(method)
|
||||||
ctx.path = cn.ppath
|
ctx.path = cn.ppath
|
||||||
ctx.pnames = cn.pnames
|
ctx.pnames = cn.pnames
|
||||||
|
|
||||||
// NOTE: Slow zone...
|
|
||||||
if ctx.handler == nil {
|
if ctx.handler == nil {
|
||||||
ctx.handler = cn.checkMethodNotAllowed()
|
ctx.handler = cn.checkMethodNotAllowed()
|
||||||
|
|
||||||
// Dig further for any, might have an empty value for *, e.g.
|
|
||||||
// serving a directory. Issue #207.
|
|
||||||
if cn = cn.anyChildren; cn == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if h := cn.findHandler(method); h != nil {
|
|
||||||
ctx.handler = h
|
|
||||||
} else {
|
|
||||||
ctx.handler = cn.checkMethodNotAllowed()
|
|
||||||
}
|
|
||||||
ctx.path = cn.ppath
|
|
||||||
ctx.pnames = cn.pnames
|
|
||||||
pvalues[len(cn.pnames)-1] = ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
176
router_test.go
176
router_test.go
@ -730,26 +730,58 @@ func TestRouterMatchAny(t *testing.T) {
|
|||||||
r := e.router
|
r := e.router
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
r.Add(http.MethodGet, "/", func(Context) error {
|
r.Add(http.MethodGet, "/", handlerHelper("case", 1))
|
||||||
return nil
|
r.Add(http.MethodGet, "/*", handlerHelper("case", 2))
|
||||||
})
|
r.Add(http.MethodGet, "/users/*", handlerHelper("case", 3))
|
||||||
r.Add(http.MethodGet, "/*", func(Context) error {
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
r.Add(http.MethodGet, "/users/*", func(Context) error {
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
c := e.NewContext(nil, nil).(*context)
|
c := e.NewContext(nil, nil).(*context)
|
||||||
r.Find(http.MethodGet, "/", c)
|
r.Find(http.MethodGet, "/", c)
|
||||||
assert.Equal(t, "", c.Param("*"))
|
c.handler(c)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, c.Get("case"))
|
||||||
|
assert.Equal(t, "/", c.Get("path"))
|
||||||
|
|
||||||
r.Find(http.MethodGet, "/download", c)
|
r.Find(http.MethodGet, "/download", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, 2, c.Get("case"))
|
||||||
|
assert.Equal(t, "/*", c.Get("path"))
|
||||||
assert.Equal(t, "download", c.Param("*"))
|
assert.Equal(t, "download", c.Param("*"))
|
||||||
|
|
||||||
r.Find(http.MethodGet, "/users/joe", c)
|
r.Find(http.MethodGet, "/users/joe", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, 3, c.Get("case"))
|
||||||
|
assert.Equal(t, "/users/*", c.Get("path"))
|
||||||
assert.Equal(t, "joe", c.Param("*"))
|
assert.Equal(t, "joe", c.Param("*"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: this is to document current implementation. Last added route with `*` asterisk is always the match and no
|
||||||
|
// backtracking or more precise matching is done to find more suitable match.
|
||||||
|
//
|
||||||
|
// Current behaviour might not be correct or expected.
|
||||||
|
// But this is where we are without well defined requirements/rules how (multiple) asterisks work in route
|
||||||
|
func TestRouterAnyMatchesLastAddedAnyRoute(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
r := e.router
|
||||||
|
|
||||||
|
r.Add(http.MethodGet, "/users/*", handlerHelper("case", 1))
|
||||||
|
r.Add(http.MethodGet, "/users/*/action*", handlerHelper("case", 2))
|
||||||
|
|
||||||
|
c := e.NewContext(nil, nil).(*context)
|
||||||
|
|
||||||
|
r.Find(http.MethodGet, "/users/xxx/action/sea", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, "/users/*/action*", c.Get("path"))
|
||||||
|
assert.Equal(t, "xxx/action/sea", c.Param("*"))
|
||||||
|
|
||||||
|
// if we add another route then it is the last added and so it is matched
|
||||||
|
r.Add(http.MethodGet, "/users/*/action/search", handlerHelper("case", 3))
|
||||||
|
|
||||||
|
r.Find(http.MethodGet, "/users/xxx/action/sea", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, "/users/*/action/search", c.Get("path"))
|
||||||
|
assert.Equal(t, "xxx/action/sea", c.Param("*"))
|
||||||
|
}
|
||||||
|
|
||||||
// Issue #1739
|
// Issue #1739
|
||||||
func TestRouterMatchAnyPrefixIssue(t *testing.T) {
|
func TestRouterMatchAnyPrefixIssue(t *testing.T) {
|
||||||
e := New()
|
e := New()
|
||||||
@ -791,6 +823,130 @@ func TestRouterMatchAnyPrefixIssue(t *testing.T) {
|
|||||||
assert.Equal(t, "users_prefix/", c.Param("*"))
|
assert.Equal(t, "users_prefix/", c.Param("*"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRouteMultiLevelBacktracking(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
r := e.router
|
||||||
|
|
||||||
|
r.Add(http.MethodGet, "/a/:b/c", handlerHelper("case", 1))
|
||||||
|
r.Add(http.MethodGet, "/a/c/d", handlerHelper("case", 2))
|
||||||
|
r.Add(http.MethodGet, "/:e/c/f", handlerHelper("case", 3))
|
||||||
|
|
||||||
|
c := e.NewContext(nil, nil).(*context)
|
||||||
|
r.Find(http.MethodGet, "/a/c/f", c)
|
||||||
|
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, 3, c.Get("case"))
|
||||||
|
assert.Equal(t, "/:e/c/f", c.Get("path"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue #
|
||||||
|
func TestRouterBacktrackingFromParam(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
r := e.router
|
||||||
|
|
||||||
|
r.Add(http.MethodGet, "/*", handlerHelper("case", 1))
|
||||||
|
r.Add(http.MethodGet, "/users/:name/", handlerHelper("case", 2))
|
||||||
|
|
||||||
|
c := e.NewContext(nil, nil).(*context)
|
||||||
|
|
||||||
|
r.Find(http.MethodGet, "/users/firstname/no-match", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, 1, c.Get("case"))
|
||||||
|
assert.Equal(t, "/*", c.Get("path"))
|
||||||
|
assert.Equal(t, "users/firstname/no-match", c.Param("*"))
|
||||||
|
|
||||||
|
r.Find(http.MethodGet, "/users/firstname/", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, 2, c.Get("case"))
|
||||||
|
assert.Equal(t, "/users/:name/", c.Get("path"))
|
||||||
|
assert.Equal(t, "firstname", c.Param("name"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterBacktrackingFromParamAny(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
r := e.router
|
||||||
|
|
||||||
|
r.Add(http.MethodGet, "/*", handlerHelper("case", 1))
|
||||||
|
r.Add(http.MethodGet, "/:name/lastname", handlerHelper("case", 2))
|
||||||
|
|
||||||
|
c := e.NewContext(nil, nil).(*context)
|
||||||
|
|
||||||
|
r.Find(http.MethodGet, "/firstname/test", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, 1, c.Get("case"))
|
||||||
|
assert.Equal(t, "/*", c.Get("path"))
|
||||||
|
assert.Equal(t, "firstname/test", c.Param("*"))
|
||||||
|
|
||||||
|
r.Find(http.MethodGet, "/firstname", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, 1, c.Get("case"))
|
||||||
|
assert.Equal(t, "/*", c.Get("path"))
|
||||||
|
assert.Equal(t, "firstname", c.Param("*"))
|
||||||
|
|
||||||
|
r.Find(http.MethodGet, "/firstname/lastname", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, 2, c.Get("case"))
|
||||||
|
assert.Equal(t, "/:name/lastname", c.Get("path"))
|
||||||
|
assert.Equal(t, "firstname", c.Param("name"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterBacktrackingFromParamAny2(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
r := e.router
|
||||||
|
|
||||||
|
r.Add(http.MethodGet, "/*", handlerHelper("case", 1))
|
||||||
|
r.Add(http.MethodGet, "/:name", handlerHelper("case", 2))
|
||||||
|
r.Add(http.MethodGet, "/:name/lastname", handlerHelper("case", 3))
|
||||||
|
|
||||||
|
c := e.NewContext(nil, nil).(*context)
|
||||||
|
|
||||||
|
r.Find(http.MethodGet, "/firstname/test", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, 1, c.Get("case"))
|
||||||
|
assert.Equal(t, "/*", c.Get("path"))
|
||||||
|
assert.Equal(t, "firstname/test", c.Param("*"))
|
||||||
|
|
||||||
|
r.Find(http.MethodGet, "/firstname", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, 2, c.Get("case"))
|
||||||
|
assert.Equal(t, "/:name", c.Get("path"))
|
||||||
|
assert.Equal(t, "firstname", c.Param("name"))
|
||||||
|
|
||||||
|
r.Find(http.MethodGet, "/firstname/lastname", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, 3, c.Get("case"))
|
||||||
|
assert.Equal(t, "/:name/lastname", c.Get("path"))
|
||||||
|
assert.Equal(t, "firstname", c.Param("name"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouterAnyCommonPath(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
r := e.router
|
||||||
|
|
||||||
|
r.Add(http.MethodGet, "/ab*", handlerHelper("case", 1))
|
||||||
|
r.Add(http.MethodGet, "/abcd", handlerHelper("case", 2))
|
||||||
|
r.Add(http.MethodGet, "/abcd*", handlerHelper("case", 3))
|
||||||
|
|
||||||
|
c := e.NewContext(nil, nil).(*context)
|
||||||
|
|
||||||
|
r.Find(http.MethodGet, "/abee", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, 1, c.Get("case"))
|
||||||
|
assert.Equal(t, "/ab*", c.Get("path"))
|
||||||
|
assert.Equal(t, "ee", c.Param("*"))
|
||||||
|
|
||||||
|
r.Find(http.MethodGet, "/abcd", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, "/abcd", c.Get("path"))
|
||||||
|
assert.Equal(t, 2, c.Get("case"))
|
||||||
|
|
||||||
|
r.Find(http.MethodGet, "/abcde", c)
|
||||||
|
c.handler(c)
|
||||||
|
assert.Equal(t, 3, c.Get("case"))
|
||||||
|
assert.Equal(t, "/abcd*", c.Get("path"))
|
||||||
|
assert.Equal(t, "e", c.Param("*"))
|
||||||
|
}
|
||||||
|
|
||||||
// TestRouterMatchAnySlash shall verify finding the best route
|
// TestRouterMatchAnySlash shall verify finding the best route
|
||||||
// for any routes with trailing slash requests
|
// for any routes with trailing slash requests
|
||||||
func TestRouterMatchAnySlash(t *testing.T) {
|
func TestRouterMatchAnySlash(t *testing.T) {
|
||||||
|
Reference in New Issue
Block a user