Skip to content

Commit 4f7bea5

Browse files
authored
fix(router): grow PathValues for contexts created before a route with more params (#3157)
Adding a route with more path params after Echo has served requests panicked with "index out of range" on the next request that matched it, with both DefaultRouter and ConcurrentRouter. Pooled Contexts keep the PathValues capacity they were created with. DefaultRouter.Route detected the short capacity but allocated a zero-length slice and then set values by index. The Context's PathValues are now grown at full length in that branch and kept, so later requests do not grow them again. The Router contract documents that Route must handle such Contexts. Refs #1705
1 parent 40c22f4 commit 4f7bea5

3 files changed

Lines changed: 90 additions & 3 deletions

File tree

‎echo_test.go‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1966,3 +1966,33 @@ func TestHasDotOrEmptySegment(t *testing.T) {
19661966
})
19671967
}
19681968
}
1969+
1970+
func TestEcho_AddParamRouteAfterServing(t *testing.T) {
1971+
var testCases = []struct {
1972+
name string
1973+
router Router
1974+
}{
1975+
{name: "ok, default router", router: NewRouter(RouterConfig{})},
1976+
{name: "ok, concurrent router", router: NewConcurrentRouter(NewRouter(RouterConfig{}))},
1977+
}
1978+
for _, tc := range testCases {
1979+
t.Run(tc.name, func(t *testing.T) {
1980+
e := NewWithConfig(Config{Router: tc.router})
1981+
e.GET("/static", func(c *Context) error { return c.String(http.StatusOK, "static") })
1982+
1983+
rec := httptest.NewRecorder()
1984+
e.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/static", nil))
1985+
assert.Equal(t, "static", rec.Body.String())
1986+
1987+
// the pooled context from the first request has no PathValues capacity for this route. sync.Pool may hand
1988+
// out a new context instead (often under -race); TestDefaultRouter_RouteWithContextCreatedBeforeParamRouteAdded
1989+
// covers the stale context deterministically.
1990+
e.GET("/users/:id", func(c *Context) error { return c.String(http.StatusOK, c.Param("id")) })
1991+
1992+
rec = httptest.NewRecorder()
1993+
e.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/users/42", nil))
1994+
assert.Equal(t, http.StatusOK, rec.Code)
1995+
assert.Equal(t, "42", rec.Body.String())
1996+
})
1997+
}
1998+
}

‎router.go‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@ import (
1515
// Router is interface for routing request contexts to registered routes.
1616
//
1717
// Contract between Echo/Context instance and the router:
18-
// - all routes must be added through methods on echo.Echo instance.
19-
// Reason: Echo instance uses RouteInfo.Params() length to allocate slice for paths parameters (see `Echo.contextPathParamAllocSize`).
18+
// - all routes should be added through methods on echo.Echo instance.
19+
// Reason: Echo instance uses RouteInfo.Parameters length to allocate slice for paths parameters (see `Echo.contextPathParamAllocSize`),
20+
// so Contexts are created with enough capacity.
21+
// - Router.Route must handle a Context whose PathValues capacity is smaller than the maximum path parameter count
22+
// of its routes (for example, when a route was added after the Context was created, or not through echo.Echo).
2023
// - Router must populate Context during Router.Route call with:
2124
// - Context.InitializeRoute (IMPORTANT! to reduce allocations use same slice that c.PathValues() returns)
2225
// - Optionally can set additional information to Context with Context.Set
@@ -916,7 +919,11 @@ var optionsMethodHandler = func(c *Context) error {
916919
func (r *DefaultRouter) Route(c *Context) HandlerFunc {
917920
pathValues := c.PathValues()
918921
if cap(pathValues) < r.maxPathParamsLength {
919-
pathValues = make(PathValues, 0, r.maxPathParamsLength)
922+
// The Context has less capacity than this router's routes need, for example because it was created before a
923+
// route with more params was added. Grow its PathValues at full length so values can be set by index below. The
924+
// Context keeps the new slice, so later requests do not grow it again unless a route with more params is added.
925+
*c.pathValues = make(PathValues, r.maxPathParamsLength)
926+
pathValues = *c.pathValues
920927
} else {
921928
pathValues = pathValues[0:cap(pathValues)] // resize slice to maximum capacity so we can index set values
922929
}

‎router_test.go‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2617,6 +2617,56 @@ func TestRouterParam1466(t *testing.T) {
26172617
}
26182618
}
26192619

2620+
func TestDefaultRouter_RouteWithContextCreatedBeforeParamRouteAdded(t *testing.T) {
2621+
var testCases = []struct {
2622+
name string
2623+
whenURL string
2624+
expectRoute string
2625+
expectPathValues PathValues
2626+
}{
2627+
{
2628+
name: "ok, param and any route",
2629+
whenURL: "/users/1/files/a.txt",
2630+
expectRoute: "/users/:id/files/*",
2631+
expectPathValues: PathValues{{Name: "id", Value: "1"}, {Name: "*", Value: "a.txt"}},
2632+
},
2633+
{
2634+
name: "ok, static route",
2635+
whenURL: "/static",
2636+
expectRoute: "/static",
2637+
expectPathValues: PathValues{},
2638+
},
2639+
{
2640+
name: "ok, route not found",
2641+
whenURL: "/missing",
2642+
expectRoute: "",
2643+
expectPathValues: PathValues{},
2644+
},
2645+
}
2646+
for _, tc := range testCases {
2647+
t.Run(tc.name, func(t *testing.T) {
2648+
e := New()
2649+
// the context is created while no route has path params, so its PathValues have no capacity
2650+
c := e.NewContext(httptest.NewRequest(http.MethodGet, tc.whenURL, nil), httptest.NewRecorder())
2651+
2652+
r := NewRouter(RouterConfig{})
2653+
_, err := r.Add(Route{Method: http.MethodGet, Path: "/users/:id/files/*", Handler: handlerFunc})
2654+
assert.NoError(t, err)
2655+
_, err = r.Add(Route{Method: http.MethodGet, Path: "/a/:b/:c/:d", Handler: handlerFunc})
2656+
assert.NoError(t, err)
2657+
_, err = r.Add(Route{Method: http.MethodGet, Path: "/static", Handler: handlerFunc})
2658+
assert.NoError(t, err)
2659+
2660+
r.Route(c)
2661+
2662+
assert.Equal(t, tc.expectRoute, c.Path())
2663+
assert.Equal(t, tc.expectPathValues, c.PathValues())
2664+
// the context keeps the grown capacity, so later requests do not grow it again
2665+
assert.Equal(t, 3, cap(c.PathValues()))
2666+
})
2667+
}
2668+
}
2669+
26202670
func TestPathValuesSizeOverMultipleRequests(t *testing.T) {
26212671
e := New()
26222672
e.GET("/test/:id/:action", handlerFunc) // max params is 2

0 commit comments

Comments
 (0)