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 @@ -4,4 +4,12 @@

Middleware for the [Gin web server](https://github.com/gin-gonic/gin) for use with [oapi-codegen/oapi-codegen](https://github.com/oapi-codegen/oapi-codegen).

## Validated request body

The middleware enables kin-openapi's `openapi3filter.Options.KeepValidatedValue` by default. After a request body passes validation, `c.Request.Body` stays readable (with the original bytes, unless kin-openapi rewrote it to apply schema defaults) and additionally implements `interface{ ValidatedValue() any }`, returning the decoded value that was validated. oapi-codegen strict servers that support it can bind that value instead of decoding the body a second time, so the handler sees exactly what was validated (for example, `encoding/json`'s case-insensitive member matching can no longer make a request validate as one value but bind as another).

To opt out, for instance to limit memory retention for very large request bodies, set `Options.DiscardValidatedValue = true`. This setting overrides `Options.Options.KeepValidatedValue`.

Any middleware placed after the validator that replaces `c.Request.Body` (decompression, size limiting, logging wrappers and the like) hides the validated value from handlers. Register such middleware before the validator.

Licensed under the Apache-2.0.
18 changes: 17 additions & 1 deletion oapi_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,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 @@ -140,9 +150,15 @@ func ValidateRequestFromContext(c *gin.Context, router routers.Router, options *
requestContext := context.WithValue(context.Background(), GinContextKey, c) //nolint:staticcheck

if options != nil {
validationInput.Options = &options.Options
// Copy the openapi3filter options so the caller's struct is not
// mutated when applying our KeepValidatedValue default.
filterOptions := options.Options
filterOptions.KeepValidatedValue = !options.DiscardValidatedValue
validationInput.Options = &filterOptions
validationInput.ParamDecoder = options.ParamDecoder
requestContext = context.WithValue(requestContext, UserDataKey, options.UserData) //nolint:staticcheck
} else {
validationInput.Options = &openapi3filter.Options{KeepValidatedValue: true}
}

err = openapi3filter.ValidateRequest(requestContext, validationInput)
Expand Down
98 changes: 98 additions & 0 deletions oapi_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -439,3 +439,101 @@ 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 *Options
wantValidated bool
}{
{
name: "nil options keep the validated value",
options: nil,
wantValidated: true,
},
{
name: "empty options keep the validated value",
options: &Options{SilenceServersWarning: true},
wantValidated: true,
},
{
name: "DiscardValidatedValue drops the validated value",
options: &Options{
SilenceServersWarning: true,
DiscardValidatedValue: true,
// DiscardValidatedValue overrides the openapi3filter setting.
Options: openapi3filter.Options{KeepValidatedValue: true},
},
wantValidated: false,
},
{
name: "ExcludeRequestBody skips body validation",
options: &Options{
SilenceServersWarning: true,
Options: openapi3filter.Options{ExcludeRequestBody: true},
},
wantValidated: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var originalFilterOptions openapi3filter.Options
if tt.options != nil {
originalFilterOptions = tt.options.Options
}

g := gin.New()
if tt.options == nil {
g.Use(OapiRequestValidator(swagger))
} else {
g.Use(OapiRequestValidatorWithOptions(swagger, tt.options))
}

called := false
var hasValidated bool
var validated any
var bodyBytes []byte
var bound map[string]any
g.POST("/resource", func(c *gin.Context) {
called = true
if vv, ok := c.Request.Body.(validatedValuer); ok {
hasValidated = true
validated = vv.ValidatedValue()
}
var readErr error
bodyBytes, readErr = io.ReadAll(c.Request.Body)
require.NoError(t, readErr)
require.NoError(t, json.Unmarshal(bodyBytes, &bound))
c.AbortWithStatus(http.StatusNoContent)
})

body := map[string]any{"name": "Marcin"}
rec := doPost(t, g, "http://deepmap.ai/resource", body)
assert.Equal(t, http.StatusNoContent, rec.Code)
require.True(t, called, "Handler should have been called")

wantBytes, err := json.Marshal(body)
require.NoError(t, err)
assert.Equal(t, string(wantBytes), string(bodyBytes), "body should still be readable with the original bytes")
assert.Equal(t, body, bound)

assert.Equal(t, tt.wantValidated, hasValidated)
if tt.wantValidated {
assert.Equal(t, map[string]any{"name": "Marcin"}, validated)
}

if tt.options != nil {
assert.Equal(t, originalFilterOptions.KeepValidatedValue, tt.options.Options.KeepValidatedValue,
"the caller's openapi3filter.Options must not be mutated")
}
})
}
}
Loading