diff --git a/router.go b/router.go index 99950aae7..eb5f80982 100644 --- a/router.go +++ b/router.go @@ -184,6 +184,10 @@ const ( paramLabel = byte(':') anyLabel = byte('*') + + // paramPlaceholder marks a path parameter while a route is inserted into the + // tree, so that a literal ':' from an escaped `\:` stays a static node. + paramPlaceholder = "\x00" ) type routeMethod struct { @@ -554,7 +558,7 @@ func (r *DefaultRouter) Add(route Route) (RouteInfo, error) { } paramNames = append(paramNames, path[j:i]) - path = path[:j] + path[i:] + path = path[:j-1] + paramPlaceholder + path[i:] i, lcpIndex = j, len(path) if i == lcpIndex { @@ -814,7 +818,7 @@ func (n *node) findChildWithLabel(l byte) *node { if c := n.findStaticChild(l); c != nil { return c } - if l == paramLabel { + if l == paramPlaceholder[0] { return n.paramChild } if l == anyLabel { @@ -937,8 +941,8 @@ func (r *DefaultRouter) Route(c *Context) HandlerFunc { searchIndex -= len(previous.prefix) } else { paramIndex-- - // 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 + // param/any node prefixes are a single marker byte, so restore searchIndex + // from the value stored for that param instead searchIndex -= len(pathValues[paramIndex].Value) pathValues[paramIndex].Value = "" } diff --git a/router_test.go b/router_test.go index 22c1dc759..85582c558 100644 --- a/router_test.go +++ b/router_test.go @@ -1320,6 +1320,65 @@ func TestRouterParamStaticConflict(t *testing.T) { } } +// Issue #3111 +func TestRouterParam_escapeColonAndParamConflict(t *testing.T) { + var testCases = []struct { + name string + routes []string + whenURL string + expectRoute string + expectParam map[string]string + }{ + { + name: "escaped colon route first, request escaped colon route", + routes: []string{`/name\:verb/x`, `/name:id`}, + whenURL: "/name:verb/x", + expectRoute: `/name\:verb/x`, + expectParam: map[string]string{}, + }, + { + name: "escaped colon route first, request param route", + routes: []string{`/name\:verb/x`, `/name:id`}, + whenURL: "/name1", + expectRoute: "/name:id", + expectParam: map[string]string{"id": "1"}, + }, + { + name: "param route first, request escaped colon route", + routes: []string{`/name:id`, `/name\:verb/x`}, + whenURL: "/name:verb/x", + expectRoute: `/name\:verb/x`, + expectParam: map[string]string{}, + }, + { + name: "param route first, request param route", + routes: []string{`/name:id`, `/name\:verb/x`}, + whenURL: "/name1", + expectRoute: "/name:id", + expectParam: map[string]string{"id": "1"}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + e := New() + for _, route := range tc.routes { + e.GET(route, handlerFunc) + } + + c := e.NewContext(httptest.NewRequest(http.MethodGet, tc.whenURL, nil), nil) + + handler := e.router.Route(c) + + assert.NoError(t, handler(c)) + assert.Equal(t, tc.expectRoute, c.Path()) + for param, expectedValue := range tc.expectParam { + assert.Equal(t, expectedValue, c.pathValues.GetOr(param, "---none---")) + } + checkUnusedParamValues(t, c, tc.expectParam) + }) + } +} + func TestRouterParam_escapeColon(t *testing.T) { // to allow Google cloud API like route paths with colon in them // i.e. https://service.name/v1/some/resource/name:customVerb <- that `:customVerb` is not path param. It is just a string