Skip to content

Commit 65fcc37

Browse files
author
Mark Pierce
committed
feat: support rejecting when request body present but not required by specification
1 parent 8933711 commit 65fcc37

File tree

4 files changed

+169
-16
lines changed

4 files changed

+169
-16
lines changed

.github/docs/openapi3filter.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ type Options struct {
238238
// Set RegexCompiler to override the regex implementation
239239
RegexCompiler openapi3.RegexCompilerFunc
240240

241+
// Set RejectWhenRequestBodyNotSpecified so ValidateRequest fails when request body is present but not defined in the specification
242+
RejectWhenRequestBodyNotSpecified bool
243+
241244
// A document with security schemes defined will not pass validation
242245
// unless an AuthenticationFunc is defined.
243246
// See NoopAuthenticationFunc

openapi3filter/options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type Options struct {
2828
// Set RegexCompiler to override the regex implementation
2929
RegexCompiler openapi3.RegexCompilerFunc
3030

31+
// Set RejectWhenRequestBodyNotSpecified so ValidateRequest fails when request body is present but not defined in the specification
32+
RejectWhenRequestBodyNotSpecified bool
33+
3134
// A document with security schemes defined will not pass validation
3235
// unless an AuthenticationFunc is defined.
3336
// See NoopAuthenticationFunc
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package openapi3filter
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
"testing"
7+
8+
"github.com/getkin/kin-openapi/openapi3filter"
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/getkin/kin-openapi/openapi3"
12+
"github.com/getkin/kin-openapi/routers/gorillamux"
13+
)
14+
15+
func TestIssue1100(t *testing.T) {
16+
spec := `
17+
openapi: 3.0.3
18+
info:
19+
version: 1.0.0
20+
title: sample api
21+
description: api service paths to test the issue
22+
paths:
23+
/api/path:
24+
post:
25+
summary: path
26+
tags:
27+
- api
28+
responses:
29+
'200':
30+
description: Ok
31+
`[1:]
32+
33+
loader := openapi3.NewLoader()
34+
35+
doc, err := loader.LoadFromData([]byte(spec))
36+
require.NoError(t, err)
37+
38+
err = doc.Validate(loader.Context)
39+
require.NoError(t, err)
40+
41+
router, err := gorillamux.NewRouter(doc)
42+
require.NoError(t, err)
43+
44+
for _, testcase := range []struct {
45+
name string
46+
endpoint string
47+
ct string
48+
data string
49+
rejectBody bool
50+
shouldFail bool
51+
}{
52+
{
53+
name: "json success",
54+
endpoint: "/api/path",
55+
ct: "application/json",
56+
data: ``,
57+
rejectBody: false,
58+
shouldFail: false,
59+
},
60+
{
61+
name: "json failure",
62+
endpoint: "/api/path",
63+
ct: "application/json",
64+
data: `{"data":"some+unexpected+data"}`,
65+
rejectBody: false,
66+
shouldFail: false,
67+
},
68+
{
69+
name: "json success",
70+
endpoint: "/api/path",
71+
ct: "application/json",
72+
data: ``,
73+
rejectBody: true,
74+
shouldFail: false,
75+
},
76+
{
77+
name: "json failure",
78+
endpoint: "/api/path",
79+
ct: "application/json",
80+
data: `{"data":"some+unexpected+data"}`,
81+
rejectBody: true,
82+
shouldFail: true,
83+
},
84+
} {
85+
t.Run(
86+
testcase.name, func(t *testing.T) {
87+
data := strings.NewReader(testcase.data)
88+
req, err := http.NewRequest("POST", testcase.endpoint, data)
89+
require.NoError(t, err)
90+
req.Header.Add("Content-Type", testcase.ct)
91+
92+
route, pathParams, err := router.FindRoute(req)
93+
require.NoError(t, err)
94+
95+
validationInput := &openapi3filter.RequestValidationInput{
96+
Request: req,
97+
PathParams: pathParams,
98+
Route: route,
99+
Options: &openapi3filter.Options{RejectWhenRequestBodyNotSpecified: testcase.rejectBody},
100+
}
101+
err = openapi3filter.ValidateRequest(loader.Context, validationInput)
102+
if testcase.shouldFail {
103+
require.Error(t, err, "This test case should fail "+testcase.data)
104+
} else {
105+
require.NoError(t, err, "This test case should pass "+testcase.data)
106+
}
107+
},
108+
)
109+
}
110+
}

openapi3filter/validate_request.go

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,23 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) error {
9090

9191
// RequestBody
9292
requestBody := operation.RequestBody
93-
if requestBody != nil && !options.ExcludeRequestBody {
94-
if err := ValidateRequestBody(ctx, input, requestBody.Value); err != nil {
93+
if !options.ExcludeRequestBody {
94+
// Validate specification request body if present
95+
if requestBody != nil {
96+
if err := ValidateRequestBody(ctx, input, requestBody.Value); err != nil {
97+
if !options.MultiError {
98+
return err
99+
}
100+
me = append(me, err)
101+
}
102+
}
103+
104+
// Reject if specification request body if not present (not wanted) but is present in the HTTP request
105+
if options.RejectWhenRequestBodyNotSpecified && input.Request.ContentLength > 0 {
106+
err := &RequestError{
107+
Input: input,
108+
Err: fmt.Errorf("request body not allowed for this request"),
109+
}
95110
if !options.MultiError {
96111
return err
97112
}
@@ -193,22 +208,34 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param
193208
case openapi3.ParameterInHeader:
194209
req.Header.Add(parameter.Name, fmt.Sprint(value))
195210
case openapi3.ParameterInCookie:
196-
req.AddCookie(&http.Cookie{
197-
Name: parameter.Name,
198-
Value: fmt.Sprint(value),
199-
})
211+
req.AddCookie(
212+
&http.Cookie{
213+
Name: parameter.Name,
214+
Value: fmt.Sprint(value),
215+
},
216+
)
200217
}
201218
}
202219
}
203220

204221
// Validate a parameter's value and presence.
205222
if parameter.Required && !found {
206-
return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidRequired.Error(), Err: ErrInvalidRequired}
223+
return &RequestError{
224+
Input: input,
225+
Parameter: parameter,
226+
Reason: ErrInvalidRequired.Error(),
227+
Err: ErrInvalidRequired,
228+
}
207229
}
208230

209231
if isNilValue(value) {
210232
if !parameter.AllowEmptyValue && found {
211-
return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidEmptyValue.Error(), Err: ErrInvalidEmptyValue}
233+
return &RequestError{
234+
Input: input,
235+
Parameter: parameter,
236+
Reason: ErrInvalidEmptyValue.Error(),
237+
Err: ErrInvalidEmptyValue,
238+
}
212239
}
213240
return nil
214241
}
@@ -372,7 +399,11 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req
372399
// ValidateSecurityRequirements goes through multiple OpenAPI 3 security
373400
// requirements in order and returns nil on the first valid requirement.
374401
// If no requirement is met, errors are returned in order.
375-
func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationInput, srs openapi3.SecurityRequirements) error {
402+
func ValidateSecurityRequirements(
403+
ctx context.Context,
404+
input *RequestValidationInput,
405+
srs openapi3.SecurityRequirements,
406+
) error {
376407
if len(srs) == 0 {
377408
return nil
378409
}
@@ -394,7 +425,11 @@ func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationI
394425
}
395426

396427
// validateSecurityRequirement validates a single OpenAPI 3 security requirement
397-
func validateSecurityRequirement(ctx context.Context, input *RequestValidationInput, securityRequirement openapi3.SecurityRequirement) error {
428+
func validateSecurityRequirement(
429+
ctx context.Context,
430+
input *RequestValidationInput,
431+
securityRequirement openapi3.SecurityRequirement,
432+
) error {
398433
names := make([]string, 0, len(securityRequirement))
399434
for name := range securityRequirement {
400435
names = append(names, name)
@@ -467,12 +502,14 @@ func validateSecurityRequirement(ctx context.Context, input *RequestValidationIn
467502
}
468503
}
469504

470-
if err := f(ctx, &AuthenticationInput{
471-
RequestValidationInput: input,
472-
SecuritySchemeName: name,
473-
SecurityScheme: securityScheme,
474-
Scopes: scopes,
475-
}); err != nil {
505+
if err := f(
506+
ctx, &AuthenticationInput{
507+
RequestValidationInput: input,
508+
SecuritySchemeName: name,
509+
SecurityScheme: securityScheme,
510+
Scopes: scopes,
511+
},
512+
); err != nil {
476513
return err
477514
}
478515
}

0 commit comments

Comments
 (0)