Skip to content

Commit 3be535f

Browse files
slessardsl255051
andauthored
openapi3filter: validate non-string headers (#712)
Co-authored-by: Steve Lessard <[email protected]>
1 parent 25a5fe4 commit 3be535f

File tree

5 files changed

+271
-22
lines changed

5 files changed

+271
-22
lines changed

openapi3/schema.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
10501050
return
10511051
}
10521052

1053+
// The value is not considered in visitJSONNull because according to the spec
1054+
// "null is not supported as a type" unless `nullable` is also set to true
1055+
// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#data-types
1056+
// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object
10531057
func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err error) {
10541058
if schema.Nullable {
10551059
return

openapi3filter/issue201_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func TestIssue201(t *testing.T) {
1717
loader := openapi3.NewLoader()
1818
ctx := loader.Context
1919
spec := `
20-
openapi: '3'
20+
openapi: '3.0.3'
2121
info:
2222
version: 1.0.0
2323
title: Sample API
@@ -37,20 +37,24 @@ paths:
3737
description: ''
3838
required: true
3939
schema:
40+
type: string
4041
pattern: '^blip$'
4142
x-blop:
4243
description: ''
4344
schema:
45+
type: string
4446
pattern: '^blop$'
4547
X-Blap:
4648
description: ''
4749
required: true
4850
schema:
51+
type: string
4952
pattern: '^blap$'
5053
X-Blup:
5154
description: ''
5255
required: true
5356
schema:
57+
type: string
5458
pattern: '^blup$'
5559
`[1:]
5660

@@ -94,7 +98,7 @@ paths:
9498
},
9599

96100
"invalid required header": {
97-
err: `response header "X-Blup" doesn't match the schema: string "bluuuuuup" doesn't match the regular expression "^blup$"`,
101+
err: `response header "X-Blup" doesn't match schema: string "bluuuuuup" doesn't match the regular expression "^blup$"`,
98102
headers: map[string]string{
99103
"X-Blip": "blip",
100104
"x-blop": "blop",

openapi3filter/req_resp_decoder.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho
339339
}
340340
_, found = vDecoder.values[param]
341341
case *headerParamDecoder:
342-
_, found = vDecoder.header[param]
342+
_, found = vDecoder.header[http.CanonicalHeaderKey(param)]
343343
case *cookieParamDecoder:
344344
_, err := vDecoder.req.Cookie(param)
345345
found = err != http.ErrNoCookie
@@ -888,7 +888,7 @@ func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, err
888888

889889
// parsePrimitive returns a value that is created by parsing a source string to a primitive type
890890
// that is specified by a schema. The function returns nil when the source string is empty.
891-
// The function panics when a schema has a non primitive type.
891+
// The function panics when a schema has a non-primitive type.
892892
func parsePrimitive(raw string, schema *openapi3.SchemaRef) (interface{}, error) {
893893
if raw == "" {
894894
return nil, nil

openapi3filter/validate_response.go

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -78,24 +78,10 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
7878
}
7979
}
8080
sort.Strings(headers)
81-
for _, k := range headers {
82-
s := response.Headers[k]
83-
h := input.Header.Get(k)
84-
if h == "" {
85-
if s.Value.Required {
86-
return &ResponseError{
87-
Input: input,
88-
Reason: fmt.Sprintf("response header %q missing", k),
89-
}
90-
}
91-
continue
92-
}
93-
if err := s.Value.Schema.Value.VisitJSON(h, opts...); err != nil {
94-
return &ResponseError{
95-
Input: input,
96-
Reason: fmt.Sprintf("response header %q doesn't match the schema", k),
97-
Err: err,
98-
}
81+
for _, headerName := range headers {
82+
headerRef := response.Headers[headerName]
83+
if err := validateResponseHeader(headerName, headerRef, input, opts); err != nil {
84+
return err
9985
}
10086
}
10187

@@ -171,6 +157,46 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error
171157
return nil
172158
}
173159

160+
func validateResponseHeader(headerName string, headerRef *openapi3.HeaderRef, input *ResponseValidationInput, opts []openapi3.SchemaValidationOption) error {
161+
var err error
162+
var decodedValue interface{}
163+
var found bool
164+
var sm *openapi3.SerializationMethod
165+
dec := &headerParamDecoder{header: input.Header}
166+
167+
if sm, err = headerRef.Value.SerializationMethod(); err != nil {
168+
return &ResponseError{
169+
Input: input,
170+
Reason: fmt.Sprintf("unable to get header %q serialization method", headerName),
171+
Err: err,
172+
}
173+
}
174+
175+
if decodedValue, found, err = decodeValue(dec, headerName, sm, headerRef.Value.Schema, headerRef.Value.Required); err != nil {
176+
return &ResponseError{
177+
Input: input,
178+
Reason: fmt.Sprintf("unable to decode header %q value", headerName),
179+
Err: err,
180+
}
181+
}
182+
183+
if found {
184+
if err = headerRef.Value.Schema.Value.VisitJSON(decodedValue, opts...); err != nil {
185+
return &ResponseError{
186+
Input: input,
187+
Reason: fmt.Sprintf("response header %q doesn't match schema", headerName),
188+
Err: err,
189+
}
190+
}
191+
} else if headerRef.Value.Required {
192+
return &ResponseError{
193+
Input: input,
194+
Reason: fmt.Sprintf("response header %q missing", headerName),
195+
}
196+
}
197+
return nil
198+
}
199+
174200
// getSchemaIdentifier gets something by which a schema could be identified.
175201
// A schema by itself doesn't have a true identity field. This function makes
176202
// a best effort to get a value that can fill that void.
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package openapi3filter
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/getkin/kin-openapi/openapi3"
12+
)
13+
14+
func Test_validateResponseHeader(t *testing.T) {
15+
type args struct {
16+
headerName string
17+
headerRef *openapi3.HeaderRef
18+
}
19+
tests := []struct {
20+
name string
21+
args args
22+
isHeaderPresent bool
23+
headerVals []string
24+
wantErr bool
25+
wantErrMsg string
26+
}{
27+
{
28+
name: "test required string header with single string value",
29+
args: args{
30+
headerName: "X-Blab",
31+
headerRef: newHeaderRef(openapi3.NewStringSchema(), true),
32+
},
33+
isHeaderPresent: true,
34+
headerVals: []string{"blab"},
35+
wantErr: false,
36+
},
37+
{
38+
name: "test required string header with single, empty string value",
39+
args: args{
40+
headerName: "X-Blab",
41+
headerRef: newHeaderRef(openapi3.NewStringSchema(), true),
42+
},
43+
isHeaderPresent: true,
44+
headerVals: []string{""},
45+
wantErr: true,
46+
wantErrMsg: `response header "X-Blab" doesn't match schema: Value is not nullable`,
47+
},
48+
{
49+
name: "test optional string header with single string value",
50+
args: args{
51+
headerName: "X-Blab",
52+
headerRef: newHeaderRef(openapi3.NewStringSchema(), false),
53+
},
54+
isHeaderPresent: false,
55+
headerVals: []string{"blab"},
56+
wantErr: false,
57+
},
58+
{
59+
name: "test required, but missing string header",
60+
args: args{
61+
headerName: "X-Blab",
62+
headerRef: newHeaderRef(openapi3.NewStringSchema(), true),
63+
},
64+
isHeaderPresent: false,
65+
headerVals: nil,
66+
wantErr: true,
67+
wantErrMsg: `response header "X-Blab" missing`,
68+
},
69+
{
70+
name: "test integer header with single integer value",
71+
args: args{
72+
headerName: "X-Blab",
73+
headerRef: newHeaderRef(openapi3.NewIntegerSchema(), true),
74+
},
75+
isHeaderPresent: true,
76+
headerVals: []string{"88"},
77+
wantErr: false,
78+
},
79+
{
80+
name: "test integer header with single string value",
81+
args: args{
82+
headerName: "X-Blab",
83+
headerRef: newHeaderRef(openapi3.NewIntegerSchema(), true),
84+
},
85+
isHeaderPresent: true,
86+
headerVals: []string{"blab"},
87+
wantErr: true,
88+
wantErrMsg: `unable to decode header "X-Blab" value: value blab: an invalid integer: invalid syntax`,
89+
},
90+
{
91+
name: "test int64 header with single int64 value",
92+
args: args{
93+
headerName: "X-Blab",
94+
headerRef: newHeaderRef(openapi3.NewInt64Schema(), true),
95+
},
96+
isHeaderPresent: true,
97+
headerVals: []string{"88"},
98+
wantErr: false,
99+
},
100+
{
101+
name: "test int32 header with single int32 value",
102+
args: args{
103+
headerName: "X-Blab",
104+
headerRef: newHeaderRef(openapi3.NewInt32Schema(), true),
105+
},
106+
isHeaderPresent: true,
107+
headerVals: []string{"88"},
108+
wantErr: false,
109+
},
110+
{
111+
name: "test float64 header with single float64 value",
112+
args: args{
113+
headerName: "X-Blab",
114+
headerRef: newHeaderRef(openapi3.NewFloat64Schema(), true),
115+
},
116+
isHeaderPresent: true,
117+
headerVals: []string{"88.87"},
118+
wantErr: false,
119+
},
120+
{
121+
name: "test integer header with multiple csv integer values",
122+
args: args{
123+
headerName: "X-blab",
124+
headerRef: newHeaderRef(newArraySchema(openapi3.NewIntegerSchema()), true),
125+
},
126+
isHeaderPresent: true,
127+
headerVals: []string{"87,88"},
128+
wantErr: false,
129+
},
130+
{
131+
name: "test integer header with multiple integer values",
132+
args: args{
133+
headerName: "X-blab",
134+
headerRef: newHeaderRef(newArraySchema(openapi3.NewIntegerSchema()), true),
135+
},
136+
isHeaderPresent: true,
137+
headerVals: []string{"87", "88"},
138+
wantErr: false,
139+
},
140+
{
141+
name: "test non-typed, nullable header with single string value",
142+
args: args{
143+
headerName: "X-blab",
144+
headerRef: newHeaderRef(&openapi3.Schema{Nullable: true}, true),
145+
},
146+
isHeaderPresent: true,
147+
headerVals: []string{"blab"},
148+
wantErr: false,
149+
},
150+
{
151+
name: "test required non-typed, nullable header not present",
152+
args: args{
153+
headerName: "X-blab",
154+
headerRef: newHeaderRef(&openapi3.Schema{Nullable: true}, true),
155+
},
156+
isHeaderPresent: false,
157+
headerVals: []string{"blab"},
158+
wantErr: true,
159+
wantErrMsg: `response header "X-blab" missing`,
160+
},
161+
{
162+
name: "test non-typed, non-nullable header with single string value",
163+
args: args{
164+
headerName: "X-blab",
165+
headerRef: newHeaderRef(&openapi3.Schema{Nullable: false}, true),
166+
},
167+
isHeaderPresent: true,
168+
headerVals: []string{"blab"},
169+
wantErr: true,
170+
wantErrMsg: `response header "X-blab" doesn't match schema: Value is not nullable`,
171+
},
172+
}
173+
for _, tt := range tests {
174+
t.Run(tt.name, func(t *testing.T) {
175+
input := newInputDefault()
176+
opts := []openapi3.SchemaValidationOption(nil)
177+
if tt.isHeaderPresent {
178+
input.Header = map[string][]string{http.CanonicalHeaderKey(tt.args.headerName): tt.headerVals}
179+
}
180+
181+
err := validateResponseHeader(tt.args.headerName, tt.args.headerRef, input, opts)
182+
if tt.wantErr {
183+
require.NotEmpty(t, tt.wantErrMsg, "wanted error message is not populated")
184+
require.Error(t, err)
185+
require.Contains(t, err.Error(), tt.wantErrMsg)
186+
} else {
187+
require.NoError(t, err)
188+
}
189+
})
190+
}
191+
}
192+
193+
func newInputDefault() *ResponseValidationInput {
194+
return &ResponseValidationInput{
195+
RequestValidationInput: &RequestValidationInput{
196+
Request: nil,
197+
PathParams: nil,
198+
Route: nil,
199+
},
200+
Status: 200,
201+
Header: nil,
202+
Body: io.NopCloser(strings.NewReader(`{}`)),
203+
}
204+
}
205+
206+
func newHeaderRef(schema *openapi3.Schema, required bool) *openapi3.HeaderRef {
207+
return &openapi3.HeaderRef{Value: &openapi3.Header{Parameter: openapi3.Parameter{Schema: &openapi3.SchemaRef{Value: schema}, Required: required}}}
208+
}
209+
210+
func newArraySchema(schema *openapi3.Schema) *openapi3.Schema {
211+
arraySchema := openapi3.NewArraySchema()
212+
arraySchema.Items = openapi3.NewSchemaRef("", schema)
213+
214+
return arraySchema
215+
}

0 commit comments

Comments
 (0)