1
0
mirror of https://github.com/labstack/echo.git synced 2025-11-29 22:48:07 +02:00

Allow different param names in different methods with same path scheme (#2209)

* Change methodHandler element type to methodContext

Signed-off-by: ortyomka <iurin.art@gmail.com>

* Allow different param names in the smae path with different methods

Signed-off-by: ortyomka <iurin.art@gmail.com>

* Rename methodContext to routeMethod
Add paramsCount in each node for perfomance

Signed-off-by: ortyomka <iurin.art@gmail.com>

* Add backtracking to nearest path

Signed-off-by: ortyomka <iurin.art@gmail.com>

* Remove params from NotAllowed

Signed-off-by: ortyomka <iurin.art@gmail.com>
This commit is contained in:
Artem Iurin
2022-07-11 20:25:41 +03:00
committed by GitHub
parent ddb66e1ba2
commit 9bf1e3c8ce
2 changed files with 141 additions and 98 deletions

210
router.go
View File

@@ -19,30 +19,35 @@ type (
prefix string prefix string
parent *node parent *node
staticChildren children staticChildren children
ppath string originalPath string
pnames []string methods *routeMethods
methodHandler *methodHandler
paramChild *node paramChild *node
anyChild *node anyChild *node
paramsCount int
// isLeaf indicates that node does not have child routes // isLeaf indicates that node does not have child routes
isLeaf bool isLeaf bool
// isHandler indicates that node has at least one handler registered to it // isHandler indicates that node has at least one handler registered to it
isHandler bool isHandler bool
} }
kind uint8 kind uint8
children []*node children []*node
methodHandler struct { routeMethod struct {
connect HandlerFunc ppath string
delete HandlerFunc pnames []string
get HandlerFunc handler HandlerFunc
head HandlerFunc }
options HandlerFunc routeMethods struct {
patch HandlerFunc connect *routeMethod
post HandlerFunc delete *routeMethod
propfind HandlerFunc get *routeMethod
put HandlerFunc head *routeMethod
trace HandlerFunc options *routeMethod
report HandlerFunc patch *routeMethod
post *routeMethod
propfind *routeMethod
put *routeMethod
trace *routeMethod
report *routeMethod
allowHeader string allowHeader string
} }
) )
@@ -56,7 +61,7 @@ const (
anyLabel = byte('*') anyLabel = byte('*')
) )
func (m *methodHandler) isHandler() bool { func (m *routeMethods) isHandler() bool {
return m.connect != nil || return m.connect != nil ||
m.delete != nil || m.delete != nil ||
m.get != nil || m.get != nil ||
@@ -70,7 +75,7 @@ func (m *methodHandler) isHandler() bool {
m.report != nil m.report != nil
} }
func (m *methodHandler) updateAllowHeader() { func (m *routeMethods) updateAllowHeader() {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
buf.WriteString(http.MethodOptions) buf.WriteString(http.MethodOptions)
@@ -119,7 +124,7 @@ func (m *methodHandler) updateAllowHeader() {
func NewRouter(e *Echo) *Router { func NewRouter(e *Echo) *Router {
return &Router{ return &Router{
tree: &node{ tree: &node{
methodHandler: new(methodHandler), methods: new(routeMethods),
}, },
routes: map[string]*Route{}, routes: map[string]*Route{},
echo: e, echo: e,
@@ -153,7 +158,7 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
} }
j := i + 1 j := i + 1
r.insert(method, path[:i], nil, staticKind, "", nil) r.insert(method, path[:i], staticKind, routeMethod{})
for ; i < lcpIndex && path[i] != '/'; i++ { for ; i < lcpIndex && path[i] != '/'; i++ {
} }
@@ -163,23 +168,23 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
if i == lcpIndex { if i == lcpIndex {
// path node is last fragment of route path. ie. `/users/:id` // path node is last fragment of route path. ie. `/users/:id`
r.insert(method, path[:i], h, paramKind, ppath, pnames) r.insert(method, path[:i], paramKind, routeMethod{ppath, pnames, h})
} else { } else {
r.insert(method, path[:i], nil, paramKind, "", nil) r.insert(method, path[:i], paramKind, routeMethod{})
} }
} else if path[i] == '*' { } else if path[i] == '*' {
r.insert(method, path[:i], nil, staticKind, "", nil) r.insert(method, path[:i], staticKind, routeMethod{})
pnames = append(pnames, "*") pnames = append(pnames, "*")
r.insert(method, path[:i+1], h, anyKind, ppath, pnames) r.insert(method, path[:i+1], anyKind, routeMethod{ppath, pnames, h})
} }
} }
r.insert(method, path, h, staticKind, ppath, pnames) r.insert(method, path, staticKind, routeMethod{ppath, pnames, h})
} }
func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string, pnames []string) { func (r *Router) insert(method, path string, t kind, rm routeMethod) {
// Adjust max param // Adjust max param
paramLen := len(pnames) paramLen := len(rm.pnames)
if *r.echo.maxParam < paramLen { if *r.echo.maxParam < paramLen {
*r.echo.maxParam = paramLen *r.echo.maxParam = paramLen
} }
@@ -207,11 +212,11 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
// At root node // At root node
currentNode.label = search[0] currentNode.label = search[0]
currentNode.prefix = search currentNode.prefix = search
if h != nil { if rm.handler != nil {
currentNode.kind = t currentNode.kind = t
currentNode.addHandler(method, h) currentNode.addMethod(method, &rm)
currentNode.ppath = ppath currentNode.paramsCount = len(rm.pnames)
currentNode.pnames = pnames currentNode.originalPath = rm.ppath
} }
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
} else if lcpLen < prefixLen { } else if lcpLen < prefixLen {
@@ -221,9 +226,9 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
currentNode.prefix[lcpLen:], currentNode.prefix[lcpLen:],
currentNode, currentNode,
currentNode.staticChildren, currentNode.staticChildren,
currentNode.methodHandler, currentNode.originalPath,
currentNode.ppath, currentNode.methods,
currentNode.pnames, currentNode.paramsCount,
currentNode.paramChild, currentNode.paramChild,
currentNode.anyChild, currentNode.anyChild,
) )
@@ -243,9 +248,9 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
currentNode.label = currentNode.prefix[0] currentNode.label = currentNode.prefix[0]
currentNode.prefix = currentNode.prefix[:lcpLen] currentNode.prefix = currentNode.prefix[:lcpLen]
currentNode.staticChildren = nil currentNode.staticChildren = nil
currentNode.methodHandler = new(methodHandler) currentNode.originalPath = ""
currentNode.ppath = "" currentNode.methods = new(routeMethods)
currentNode.pnames = nil currentNode.paramsCount = 0
currentNode.paramChild = nil currentNode.paramChild = nil
currentNode.anyChild = nil currentNode.anyChild = nil
currentNode.isLeaf = false currentNode.isLeaf = false
@@ -257,13 +262,19 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
if lcpLen == searchLen { if lcpLen == searchLen {
// At parent node // At parent node
currentNode.kind = t currentNode.kind = t
currentNode.addHandler(method, h) if rm.handler != nil {
currentNode.ppath = ppath currentNode.addMethod(method, &rm)
currentNode.pnames = pnames currentNode.paramsCount = len(rm.pnames)
currentNode.originalPath = rm.ppath
}
} else { } else {
// Create child node // Create child node
n = newNode(t, search[lcpLen:], currentNode, nil, new(methodHandler), ppath, pnames, nil, nil) n = newNode(t, search[lcpLen:], currentNode, nil, "", new(routeMethods), 0, nil, nil)
n.addHandler(method, h) if rm.handler != nil {
n.addMethod(method, &rm)
n.paramsCount = len(rm.pnames)
n.originalPath = rm.ppath
}
// Only Static children could reach here // Only Static children could reach here
currentNode.addStaticChild(n) currentNode.addStaticChild(n)
} }
@@ -277,8 +288,12 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
continue continue
} }
// Create child node // Create child node
n := newNode(t, search, currentNode, nil, new(methodHandler), ppath, pnames, nil, nil) n := newNode(t, search, currentNode, nil, rm.ppath, new(routeMethods), 0, nil, nil)
n.addHandler(method, h) if rm.handler != nil {
n.addMethod(method, &rm)
n.paramsCount = len(rm.pnames)
}
switch t { switch t {
case staticKind: case staticKind:
currentNode.addStaticChild(n) currentNode.addStaticChild(n)
@@ -290,28 +305,26 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil currentNode.isLeaf = currentNode.staticChildren == nil && currentNode.paramChild == nil && currentNode.anyChild == nil
} else { } else {
// Node already exists // Node already exists
if h != nil { if rm.handler != nil {
currentNode.addHandler(method, h) currentNode.addMethod(method, &rm)
currentNode.ppath = ppath currentNode.paramsCount = len(rm.pnames)
if len(currentNode.pnames) == 0 { // Issue #729 currentNode.originalPath = rm.ppath
currentNode.pnames = pnames
}
} }
} }
return return
} }
} }
func newNode(t kind, pre string, p *node, sc children, mh *methodHandler, ppath string, pnames []string, paramChildren, anyChildren *node) *node { func newNode(t kind, pre string, p *node, sc children, originalPath string, mh *routeMethods, paramsCount int, paramChildren, anyChildren *node) *node {
return &node{ return &node{
kind: t, kind: t,
label: pre[0], label: pre[0],
prefix: pre, prefix: pre,
parent: p, parent: p,
staticChildren: sc, staticChildren: sc,
ppath: ppath, originalPath: originalPath,
pnames: pnames, methods: mh,
methodHandler: mh, paramsCount: paramsCount,
paramChild: paramChildren, paramChild: paramChildren,
anyChild: anyChildren, anyChild: anyChildren,
isLeaf: sc == nil && paramChildren == nil && anyChildren == nil, isLeaf: sc == nil && paramChildren == nil && anyChildren == nil,
@@ -345,64 +358,60 @@ func (n *node) findChildWithLabel(l byte) *node {
return nil return nil
} }
func (n *node) addHandler(method string, h HandlerFunc) { func (n *node) addMethod(method string, h *routeMethod) {
switch method { switch method {
case http.MethodConnect: case http.MethodConnect:
n.methodHandler.connect = h n.methods.connect = h
case http.MethodDelete: case http.MethodDelete:
n.methodHandler.delete = h n.methods.delete = h
case http.MethodGet: case http.MethodGet:
n.methodHandler.get = h n.methods.get = h
case http.MethodHead: case http.MethodHead:
n.methodHandler.head = h n.methods.head = h
case http.MethodOptions: case http.MethodOptions:
n.methodHandler.options = h n.methods.options = h
case http.MethodPatch: case http.MethodPatch:
n.methodHandler.patch = h n.methods.patch = h
case http.MethodPost: case http.MethodPost:
n.methodHandler.post = h n.methods.post = h
case PROPFIND: case PROPFIND:
n.methodHandler.propfind = h n.methods.propfind = h
case http.MethodPut: case http.MethodPut:
n.methodHandler.put = h n.methods.put = h
case http.MethodTrace: case http.MethodTrace:
n.methodHandler.trace = h n.methods.trace = h
case REPORT: case REPORT:
n.methodHandler.report = h n.methods.report = h
} }
n.methodHandler.updateAllowHeader() n.methods.updateAllowHeader()
if h != nil { n.isHandler = true
n.isHandler = true
} else {
n.isHandler = n.methodHandler.isHandler()
}
} }
func (n *node) findHandler(method string) HandlerFunc { func (n *node) findMethod(method string) *routeMethod {
switch method { switch method {
case http.MethodConnect: case http.MethodConnect:
return n.methodHandler.connect return n.methods.connect
case http.MethodDelete: case http.MethodDelete:
return n.methodHandler.delete return n.methods.delete
case http.MethodGet: case http.MethodGet:
return n.methodHandler.get return n.methods.get
case http.MethodHead: case http.MethodHead:
return n.methodHandler.head return n.methods.head
case http.MethodOptions: case http.MethodOptions:
return n.methodHandler.options return n.methods.options
case http.MethodPatch: case http.MethodPatch:
return n.methodHandler.patch return n.methods.patch
case http.MethodPost: case http.MethodPost:
return n.methodHandler.post return n.methods.post
case PROPFIND: case PROPFIND:
return n.methodHandler.propfind return n.methods.propfind
case http.MethodPut: case http.MethodPut:
return n.methodHandler.put return n.methods.put
case http.MethodTrace: case http.MethodTrace:
return n.methodHandler.trace return n.methods.trace
case REPORT: case REPORT:
return n.methodHandler.report return n.methods.report
default: default:
return nil return nil
} }
@@ -433,7 +442,7 @@ func (r *Router) Find(method, path string, c Context) {
var ( var (
previousBestMatchNode *node previousBestMatchNode *node
matchedHandler HandlerFunc matchedRouteMethod *routeMethod
// search stores the remaining path to check for match. By each iteration we move from start of path to end of the path // search stores the remaining path to check for match. By each iteration we move from start of path to end of the path
// and search value gets shorter and shorter. // and search value gets shorter and shorter.
search = path search = path
@@ -529,8 +538,8 @@ func (r *Router) Find(method, path string, c Context) {
if previousBestMatchNode == nil { if previousBestMatchNode == nil {
previousBestMatchNode = currentNode previousBestMatchNode = currentNode
} }
if h := currentNode.findHandler(method); h != nil { if h := currentNode.findMethod(method); h != nil {
matchedHandler = h matchedRouteMethod = h
break break
} }
} }
@@ -569,7 +578,8 @@ func (r *Router) Find(method, path string, c Context) {
if child := currentNode.anyChild; child != nil { if child := currentNode.anyChild; child != nil {
// If any node is found, use remaining path for paramValues // If any node is found, use remaining path for paramValues
currentNode = child currentNode = child
paramValues[len(currentNode.pnames)-1] = search paramValues[currentNode.paramsCount-1] = search
// update indexes/search in case we need to backtrack when no handler match is found // update indexes/search in case we need to backtrack when no handler match is found
paramIndex++ paramIndex++
searchIndex += +len(search) searchIndex += +len(search)
@@ -580,8 +590,8 @@ func (r *Router) Find(method, path string, c Context) {
if previousBestMatchNode == nil { if previousBestMatchNode == nil {
previousBestMatchNode = currentNode previousBestMatchNode = currentNode
} }
if h := currentNode.findHandler(method); h != nil { if h := currentNode.findMethod(method); h != nil {
matchedHandler = h matchedRouteMethod = h
break break
} }
} }
@@ -604,22 +614,28 @@ func (r *Router) Find(method, path string, c Context) {
return // nothing matched at all return // nothing matched at all
} }
if matchedHandler != nil { var rPath string
ctx.handler = matchedHandler var rPNames []string
if matchedRouteMethod != nil {
ctx.handler = matchedRouteMethod.handler
rPath = matchedRouteMethod.ppath
rPNames = matchedRouteMethod.pnames
} else { } else {
// use previous match as basis. although we have no matching handler we have path match. // use previous match as basis. although we have no matching handler we have path match.
// so we can send http.StatusMethodNotAllowed (405) instead of http.StatusNotFound (404) // so we can send http.StatusMethodNotAllowed (405) instead of http.StatusNotFound (404)
currentNode = previousBestMatchNode currentNode = previousBestMatchNode
rPath = currentNode.originalPath
rPNames = nil // no params here
ctx.handler = NotFoundHandler ctx.handler = NotFoundHandler
if currentNode.isHandler { if currentNode.isHandler {
ctx.Set(ContextKeyHeaderAllow, currentNode.methodHandler.allowHeader) ctx.Set(ContextKeyHeaderAllow, currentNode.methods.allowHeader)
ctx.handler = MethodNotAllowedHandler ctx.handler = MethodNotAllowedHandler
if method == http.MethodOptions { if method == http.MethodOptions {
ctx.handler = optionsMethodHandler(currentNode.methodHandler.allowHeader) ctx.handler = optionsMethodHandler(currentNode.methods.allowHeader)
} }
} }
} }
ctx.path = currentNode.ppath ctx.path = rPath
ctx.pnames = currentNode.pnames ctx.pnames = rPNames
} }

View File

@@ -2318,6 +2318,33 @@ func TestRouterPanicWhenParamNoRootOnlyChildsFailsFind(t *testing.T) {
} }
} }
// Issue #1726
func TestRouterDifferentParamsInPath(t *testing.T) {
e := New()
r := e.router
r.Add(http.MethodPut, "/*", func(Context) error {
return nil
})
r.Add(http.MethodPut, "/users/:vid/files/:gid", func(Context) error {
return nil
})
r.Add(http.MethodGet, "/users/:uid/files/:fid", func(Context) error {
return nil
})
c := e.NewContext(nil, nil).(*context)
r.Find(http.MethodGet, "/users/1/files/2", c)
assert.Equal(t, "1", c.Param("uid"))
assert.Equal(t, "2", c.Param("fid"))
r.Find(http.MethodGet, "/users/1/shouldBacktrackToFirstAnyRouteAnd405", c)
assert.Equal(t, "/*", c.Path())
r.Find(http.MethodPut, "/users/3/files/4", c)
assert.Equal(t, "3", c.Param("vid"))
assert.Equal(t, "4", c.Param("gid"))
}
func TestRouterHandleMethodOptions(t *testing.T) { func TestRouterHandleMethodOptions(t *testing.T) {
e := New() e := New()
r := e.router r := e.router
@@ -2380,7 +2407,7 @@ func TestRouterHandleMethodOptions(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tc.expectStatus, rec.Code) assert.Equal(t, tc.expectStatus, rec.Code)
} }
assert.Equal(t, tc.expectAllowHeader, c.Response().Header().Get("Allow")) assert.Equal(t, tc.expectAllowHeader, c.Response().Header().Get(HeaderAllow))
}) })
} }
} }