diff --git a/oapi_validate.go b/oapi_validate.go index 614065b..b44fbec 100644 --- a/oapi_validate.go +++ b/oapi_validate.go @@ -132,10 +132,19 @@ func ValidateRequestFromContext(ctx *echo.Context, router routers.Router, option req := ctx.Request() if options != nil && options.Prefix != "" { - // Clone the request so downstream handlers still see the original path. - clone := req.Clone(req.Context()) - clone.URL.Path = strings.TrimPrefix(clone.URL.Path, options.Prefix) - req = clone + // Trim the prefix in place rather than on a clone: Request.Clone copies + // the Body reference, so openapi3filter's security validation would read + // the original request's body and restore a fresh reader onto the clone + // only, leaving the handler a drained body. The deferred restore runs + // before the handler, which therefore still sees the original path. + path, rawPath := req.URL.Path, req.URL.RawPath + req.URL.Path = strings.TrimPrefix(path, options.Prefix) + if rawPath != "" { + req.URL.RawPath = strings.TrimPrefix(rawPath, options.Prefix) + } + defer func() { + req.URL.Path, req.URL.RawPath = path, rawPath + }() } route, pathParams, err := router.FindRoute(req) diff --git a/prefix_body_test.go b/prefix_body_test.go new file mode 100644 index 0000000..54b0edf --- /dev/null +++ b/prefix_body_test.go @@ -0,0 +1,73 @@ +package echomiddleware + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/labstack/echo/v5" + "github.com/stretchr/testify/require" +) + +func TestPrefixKeepsTheRequestBody(t *testing.T) { + const specYAML = ` +openapi: "3.0.0" +info: + title: t + version: 1.0.0 +paths: + /thing: + post: + operationId: createThing + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + responses: + '204': + description: ok +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer +` + spec, err := openapi3.NewLoader().LoadFromData([]byte(specYAML)) + require.NoError(t, err) + + e := echo.New() + e.Use(OapiRequestValidatorWithOptions(spec, &Options{ + Prefix: "/api", + SilenceServersWarning: true, + Options: openapi3filter.Options{ + ExcludeRequestBody: true, + AuthenticationFunc: func(context.Context, *openapi3filter.AuthenticationInput) error { + return nil + }, + }, + })) + + var got []byte + e.POST("/api/thing", func(c *echo.Context) error { + got, _ = io.ReadAll(c.Request().Body) + return c.NoContent(http.StatusNoContent) + }) + + r := httptest.NewRequest(http.MethodPost, "/api/thing", strings.NewReader(`{"name":"x"}`)) + r.Header.Set("content-type", "application/json") + r.Header.Set("authorization", "Bearer token") + rec := httptest.NewRecorder() + e.ServeHTTP(rec, r) + + require.Equal(t, http.StatusNoContent, rec.Code) + require.NotEmpty(t, got, "the handler received a drained body") +}