diff --git a/echo_test.go b/echo_test.go index b7f32017a..71892bf25 100644 --- a/echo_test.go +++ b/echo_test.go @@ -1860,3 +1860,20 @@ func BenchmarkEchoGitHubAPIMisses(b *testing.B) { func BenchmarkEchoParseAPI(b *testing.B) { benchmarkEchoRoutes(b, parseAPI) } + +func TestEchoAddParamRouteAfterServing(t *testing.T) { + e := New() + e.GET("/static", func(c Context) error { return c.String(http.StatusOK, "static") }) + + rec := httptest.NewRecorder() + e.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/static", nil)) + assert.Equal(t, "static", rec.Body.String()) + + // the pooled context from the first request has param values sized for routes without params + e.GET("/users/:id", func(c Context) error { return c.String(http.StatusOK, c.Param("id")) }) + + rec = httptest.NewRecorder() + e.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/users/42", nil)) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "42", rec.Body.String()) +} diff --git a/router.go b/router.go index de0824d79..0c8eadb3d 100644 --- a/router.go +++ b/router.go @@ -582,6 +582,16 @@ func optionsMethodHandler(allowMethods string) func(c Context) error { } } +// growParamValues replaces the context's pvalues when the context was created (and pooled) before a route with more +// path params was added, so Find can set values by index. Values already set during this Find are kept and the +// context keeps the new slice for later requests. +func (r *Router) growParamValues(ctx *context, paramValues []string) []string { + grown := make([]string, *r.echo.maxParam) + copy(grown, paramValues) + ctx.pvalues = grown + return grown +} + // Find lookup a handler registered for method and path. It also parses URL for path // parameters and load them into context. // @@ -730,6 +740,9 @@ func (r *Router) Find(method, path string, c Context) { i = l } + if paramIndex >= len(paramValues) { + paramValues = r.growParamValues(ctx, paramValues) + } paramValues[paramIndex] = search[:i] paramIndex++ search = search[i:] @@ -742,6 +755,9 @@ func (r *Router) Find(method, path string, c Context) { if child := currentNode.anyChild; child != nil { // If any node is found, use remaining path for paramValues currentNode = child + if currentNode.paramsCount > len(paramValues) { + paramValues = r.growParamValues(ctx, paramValues) + } paramValues[currentNode.paramsCount-1] = search // update indexes/search in case we need to backtrack when no handler match is found diff --git a/router_test.go b/router_test.go index 04c63d6fd..9c0cc1aae 100644 --- a/router_test.go +++ b/router_test.go @@ -2930,3 +2930,43 @@ func BenchmarkRouterGooglePlusAPIMisses(b *testing.B) { func BenchmarkRouterParamsAndAnyAPI(b *testing.B) { benchmarkRouterRoutes(b, paramAndAnyAPI, paramAndAnyAPIToFind) } + +func TestRouterFindWithContextCreatedBeforeParamRouteAdded(t *testing.T) { + var testCases = []struct { + name string + whenURL string + expectRoute string + expectParam map[string]string + }{ + { + name: "ok, param route grows the context", + whenURL: "/users/1/files/a.txt", + expectRoute: "/users/:id/files/*", + expectParam: map[string]string{"id": "1", "*": "a.txt"}, + }, + { + name: "ok, any route grows the context", + whenURL: "/static/css/a.css", + expectRoute: "/static/*", + expectParam: map[string]string{"*": "css/a.css"}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + e := New() + // the context is created while no route has path params, so it has no room for param values + c := e.NewContext(nil, nil).(*context) + + r := e.router + r.Add(http.MethodGet, "/users/:id/files/*", handlerFunc) + r.Add(http.MethodGet, "/static/*", handlerFunc) + + r.Find(http.MethodGet, tc.whenURL, c) + + assert.Equal(t, tc.expectRoute, c.Path()) + for name, value := range tc.expectParam { + assert.Equal(t, value, c.Param(name)) + } + }) + } +}