Skip to content

Options.Prefix drains the request body before the handler sees it #16

Description

@blacksud0

When Options.Prefix is set and the spec carries a security scheme, every POST/PUT/PATCH through the prefixed mount reaches its handler with a zero-byte body.

Mechanism. ValidateRequestFromContext handles Prefix by cloning the request and trimming the clone's path, so downstream code keeps the original URL. But http.Request.Clone copies the Body reference: when a security requirement is present, openapi3filter.validateSecurityRequirement does io.ReadAll(input.Request.Body) before invoking the AuthenticationFunc, then restores a fresh reader — onto the clone. The original request — the one the echo context still holds and the one the handler binds from — is left holding a drained reader with its ContentLength intact. This happens even with ExcludeRequestBody: true, because the read belongs to the security path, not body validation.

Why the suite misses it. TestOapiRequestValidatorWithPrefix only sends GETs; no test sends a body through a prefixed mount.

Reproduction (fails on v0.1.1 with Should NOT be empty, but was []):

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")
}

The fix we run in production (a vendored copy of this file): trim req.URL.Path — and RawPath, which the clone approach leaves inconsistent with the trimmed Pathin place, restoring both in a defer. RequestValidationInput.Request is then the live request, so openapi3filter's own body restore lands where it is needed, and handlers still observe the original path because the restore runs before next (your existing prefix-path assertion keeps passing). Eleven lines, one behaviour. Happy to send the PR with the fix and the test above if wanted.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions