Skip to content
Merged
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@
Middleware for the [Iris web server](https://github.com/kataras/iris/) for use with [oapi-codegen](https://github.com/oapi-codegen/oapi-codegen).

Licensed under the Apache-2.0.

## Validated request body

The middleware enables kin-openapi's `openapi3filter.Options.KeepValidatedValue` by default. After a request body passes validation, the body that handlers see (`ctx.Request().Body`) exposes the decoded value that was validated through a `ValidatedValue() any` method, and remains readable with its original bytes. An [oapi-codegen](https://github.com/oapi-codegen/oapi-codegen) strict server can bind that value instead of decoding the body a second time, so the handler receives exactly what was validated.

To turn this off, for instance to limit memory retention for very large request bodies, set `DiscardValidatedValue: true` in `Options`. This setting overrides `Options.Options.KeepValidatedValue`.

Middleware that replaces the request body after the validator has run (decompression, size limiting, logging wrappers and the like) hides the validated value from handlers. Register such middleware before the validator.
19 changes: 18 additions & 1 deletion oapi_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ type Options struct {
MultiErrorHandler MultiErrorHandler
// SilenceServersWarning allows silencing a warning for https://github.com/deepmap/oapi-codegen/issues/882 that reports when an OpenAPI spec has `spec.Servers != nil`
SilenceServersWarning bool
// DiscardValidatedValue turns off openapi3filter.Options.KeepValidatedValue,
// which this middleware enables by default. With it enabled, the request body
// seen by handlers exposes the value that was validated through a
// ValidatedValue() method, so a handler (for example an oapi-codegen strict
// server) can bind that value instead of decoding the body a second time.
// Set this to drop the decoded value after validation, for instance to limit
// memory retention for very large request bodies.
//
// Options.Options.KeepValidatedValue is overridden by this setting.
DiscardValidatedValue bool
}

// OapiRequestValidatorWithOptions creates a validator from a swagger object, with validation options
Expand Down Expand Up @@ -112,9 +122,16 @@ func ValidateRequestFromContext(ctx iris.Context, router routers.Router, options
requestContext := context.WithValue(context.Background(), IrisContextKey, ctx) //nolint:staticcheck

if options != nil {
requestValidationInput.Options = &options.Options
// Copy the caller's openapi3filter options so that setting
// KeepValidatedValue does not mutate the caller's struct.
filterOptions := options.Options
filterOptions.KeepValidatedValue = !options.DiscardValidatedValue
requestValidationInput.Options = &filterOptions
requestValidationInput.ParamDecoder = options.ParamDecoder
requestContext = context.WithValue(requestContext, UserDataKey, options.UserData) //nolint:staticcheck
} else {
// No options were given: keep the validated body value by default.
requestValidationInput.Options = &openapi3filter.Options{KeepValidatedValue: true}
}

err = openapi3filter.ValidateRequest(requestContext, requestValidationInput)
Expand Down
99 changes: 99 additions & 0 deletions oapi_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,102 @@ func TestOapiRequestValidatorWithOptionsMultiErrorAndCustomHandler(t *testing.T)
called = false
}
}

func TestOapiRequestValidatorKeepValidatedValue(t *testing.T) {
swagger, err := openapi3.NewLoader().LoadFromData(testSchema)
require.NoError(t, err, "Error initializing swagger")

type validatedValuer interface {
ValidatedValue() any
}

tests := []struct {
name string
// options is nil to exercise OapiRequestValidator(spec).
options *Options
// wantValidatedValue is whether the handler should see a request body
// exposing ValidatedValue().
wantValidatedValue bool
}{
{
name: "no options",
options: nil,
wantValidatedValue: true,
},
{
name: "empty options",
options: &Options{},
wantValidatedValue: true,
},
{
name: "discard validated value",
options: &Options{DiscardValidatedValue: true},
wantValidatedValue: false,
},
{
name: "exclude request body",
options: &Options{
Options: openapi3filter.Options{ExcludeRequestBody: true},
},
// The body is not validated, so there is no validated value.
wantValidatedValue: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
i := iris.New()
if tc.options == nil {
i.Use(OapiRequestValidator(swagger))
} else {
i.Use(OapiRequestValidatorWithOptions(swagger, tc.options))
}

called := false
var (
isValidatedValuer bool
validatedValue any
bodyBytes []byte
readErr error
)
i.Post("/resource", func(ctx iris.Context) {
called = true
body := ctx.Request().Body
var vv validatedValuer
vv, isValidatedValuer = body.(validatedValuer)
if isValidatedValuer {
validatedValue = vv.ValidatedValue()
}
bodyBytes, readErr = io.ReadAll(body)
ctx.StatusCode(http.StatusNoContent)
})

require.NoError(t, i.Build())

reqBody := struct {
Name string `json:"name"`
}{
Name: "Marcin",
}
res := doPost(t, i, "http://deepmap.ai/resource", reqBody)
assert.Equal(t, http.StatusNoContent, res.Code)
require.True(t, called, "Handler should have been called")

// The body must remain readable with the original bytes.
require.NoError(t, readErr)
assert.Equal(t, `{"name":"Marcin"}`, string(bodyBytes))

assert.Equal(t, tc.wantValidatedValue, isValidatedValuer,
"request body ValidatedValue() presence")
if tc.wantValidatedValue {
assert.Equal(t, map[string]any{"name": "Marcin"}, validatedValue)
}

// The caller's openapi3filter options must not be mutated.
if tc.options != nil {
assert.False(t, tc.options.Options.KeepValidatedValue,
"caller's openapi3filter.Options should not be modified")
}
})
}
}
Loading