Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 = ""
}
Expand Down
59 changes: 59 additions & 0 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down