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
17 changes: 13 additions & 4 deletions oapi_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
73 changes: 73 additions & 0 deletions prefix_body_test.go
Original file line number Diff line number Diff line change
@@ -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")
}