Skip to content

Commit 90a7df0

Browse files
kconwayatlassianfenollp
authored andcommitted
Add content-type wildcard support to validation (#93)
* Add content-type wildcard support to validation This implements the cascading wildcard behavior described in the OpenAPI specification for request body and response body validation. https://swagger.io/docs/specification/describing-request-body/ * Return nil as content type for invalid mimes Rather than allow accidental fallthrough of invalid mime types we will return nil.
1 parent 2a7fbb6 commit 90a7df0

File tree

4 files changed

+136
-20
lines changed

4 files changed

+136
-20
lines changed

openapi3/content.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,40 @@ func NewContentWithJSONSchemaRef(schema *SchemaRef) Content {
2424
}
2525

2626
func (content Content) Get(mime string) *MediaType {
27+
// Start by making the most specific match possible
28+
// by using the mime type in full.
2729
if v := content[mime]; v != nil {
2830
return v
2931
}
32+
// If an exact match is not found then we strip all
33+
// metadata from the mime type and only use the x/y
34+
// portion.
3035
i := strings.IndexByte(mime, ';')
3136
if i < 0 {
37+
// If there is no metadata then preserve the full mime type
38+
// string for later wildcard searches.
39+
i = len(mime)
40+
}
41+
mime = mime[:i]
42+
if v := content[mime]; v != nil {
43+
return v
44+
}
45+
// If the x/y pattern has no specific match then we
46+
// try the x/* pattern.
47+
i = strings.IndexByte(mime, '/')
48+
if i < 0 {
49+
// In the case that the given mime type is not valid because it is
50+
// missing the subtype we return nil so that this does not accidentally
51+
// resolve with the wildcard.
3252
return nil
3353
}
34-
return content[mime[:i]]
54+
mime = mime[:i] + "/*"
55+
if v := content[mime]; v != nil {
56+
return v
57+
}
58+
// Finally, the most generic match of */* is returned
59+
// as a catch-all.
60+
return content["*/*"]
3561
}
3662

3763
func (content Content) Validate(c context.Context) error {

openapi3/content_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package openapi3
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestContent_Get(t *testing.T) {
10+
fallback := NewMediaType()
11+
wildcard := NewMediaType()
12+
stripped := NewMediaType()
13+
fullMatch := NewMediaType()
14+
content := Content{
15+
"*/*": fallback,
16+
"application/*": wildcard,
17+
"application/json": stripped,
18+
"application/json;encoding=utf-8": fullMatch,
19+
}
20+
contentWithoutWildcards := Content{
21+
"application/json": stripped,
22+
"application/json;encoding=utf-8": fullMatch,
23+
}
24+
tests := []struct {
25+
name string
26+
content Content
27+
mime string
28+
want *MediaType
29+
}{
30+
{
31+
name: "missing",
32+
content: contentWithoutWildcards,
33+
mime: "text/plain;encoding=utf-8",
34+
want: nil,
35+
},
36+
{
37+
name: "full match",
38+
content: content,
39+
mime: "application/json;encoding=utf-8",
40+
want: fullMatch,
41+
},
42+
{
43+
name: "stripped match",
44+
content: content,
45+
mime: "application/json;encoding=utf-16",
46+
want: stripped,
47+
},
48+
{
49+
name: "wildcard match",
50+
content: content,
51+
mime: "application/yaml;encoding=utf-16",
52+
want: wildcard,
53+
},
54+
{
55+
name: "fallback match",
56+
content: content,
57+
mime: "text/plain;encoding=utf-16",
58+
want: fallback,
59+
},
60+
{
61+
name: "invalid mime type",
62+
content: content,
63+
mime: "text;encoding=utf16",
64+
want: nil,
65+
},
66+
{
67+
name: "missing no encoding",
68+
content: contentWithoutWildcards,
69+
mime: "text/plain",
70+
want: nil,
71+
},
72+
{
73+
name: "stripped match no encoding",
74+
content: content,
75+
mime: "application/json",
76+
want: stripped,
77+
},
78+
{
79+
name: "wildcard match no encoding",
80+
content: content,
81+
mime: "application/yaml",
82+
want: wildcard,
83+
},
84+
{
85+
name: "fallback match no encoding",
86+
content: content,
87+
mime: "text/plain",
88+
want: fallback,
89+
},
90+
{
91+
name: "invalid mime type no encoding",
92+
content: content,
93+
mime: "text",
94+
want: nil,
95+
},
96+
}
97+
for _, tt := range tests {
98+
t.Run(tt.name, func(t *testing.T) {
99+
// Using require.True here because require.Same is not yet released.
100+
// We're comparing pointer values and the require.Equal will
101+
// dereference and compare the pointed to values rather than check
102+
// if the memory addresses are the same. Once require.Same is released
103+
// this test should convert to using that.
104+
require.True(t, tt.want == tt.content.Get(tt.mime))
105+
})
106+
}
107+
}

openapi3filter/validate_request.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -154,16 +154,7 @@ func ValidateRequestBody(c context.Context, input *RequestValidationInput, reque
154154
}
155155

156156
inputMIME := req.Header.Get("Content-Type")
157-
mediaType := parseMediaType(inputMIME)
158-
if mediaType == "" {
159-
return &RequestError{
160-
Input: input,
161-
RequestBody: requestBody,
162-
Reason: "content type is missed",
163-
}
164-
}
165-
166-
contentType := requestBody.Content[mediaType]
157+
contentType := requestBody.Content.Get(inputMIME)
167158
if contentType == nil {
168159
return &RequestError{
169160
Input: input,

openapi3filter/validate_response.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,7 @@ func ValidateResponse(c context.Context, input *ResponseValidationInput) error {
7575
}
7676

7777
inputMIME := input.Header.Get("Content-Type")
78-
mediaType := parseMediaType(inputMIME)
79-
if mediaType == "" {
80-
return &ResponseError{
81-
Input: input,
82-
Reason: "content type of response body is missed",
83-
}
84-
}
85-
86-
contentType := content[mediaType]
78+
contentType := content.Get(inputMIME)
8779
if contentType == nil {
8880
return &ResponseError{
8981
Input: input,

0 commit comments

Comments
 (0)