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
42 changes: 41 additions & 1 deletion openapi3filter/req_resp_decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,39 @@ func getEncodingContentType(encFn EncodingFn) string {
return enc.ContentType
}

// contentTypeAllowedByEncoding reports whether mediaType satisfies the
// encoding.contentType, which per OAS 3.0 may be a single media type, a wildcard
// (e.g. image/*), or a comma-separated list of those. mediaType must be a base
// type; parameters on encoding entries (e.g. "; charset=utf-8") are ignored.
func contentTypeAllowedByEncoding(mediaType, encodingContentType string) bool {
for raw := range strings.SplitSeq(encodingContentType, ",") {
want := strings.TrimSpace(raw)
if want == "" {
continue
}
if base, _, err := mime.ParseMediaType(want); err == nil {
want = base
}
if mediaTypeMatches(mediaType, want) {
return true
}
}
return false
}

// mediaTypeMatches reports whether got matches want (exact, type/* or */* wildcard,
// case-insensitive). Both must be base media types without parameters.
func mediaTypeMatches(got, want string) bool {
if want == "*/*" || strings.EqualFold(want, got) {
return true
}
if prefix, ok := strings.CutSuffix(want, "/*"); ok {
gotType, _, found := strings.Cut(got, "/")
return found && strings.EqualFold(gotType, prefix)
}
return false
}

// decodeBody returns a decoded body.
// The function returns ParseError when a body is invalid.
func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (
Expand All @@ -1265,7 +1298,7 @@ func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef,
}

if encodingContentType != "" &&
mediaType != encodingContentType {
!contentTypeAllowedByEncoding(mediaType, encodingContentType) {
return "", nil, &ParseError{
Kind: KindOther,
Reason: fmt.Sprintf(
Expand All @@ -1279,6 +1312,13 @@ func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef,

decoder, ok := bodyDecoders[mediaType]
if !ok {
// A binary part with no registered decoder (e.g. image/png) is read as
// raw bytes: encoding.contentType restricts the accepted media types but
// does not require a registered decoder.
if isBinary(schema) {
value, err := FileBodyDecoder(body, header, schema, encFn)
return mediaType, value, err
}
return "", nil, &ParseError{
Kind: KindUnsupportedFormat,
Reason: fmt.Sprintf("%s %q", prefixUnsupportedCT, mediaType),
Expand Down
64 changes: 51 additions & 13 deletions openapi3filter/req_resp_decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1676,7 +1676,7 @@ func TestDecodeBody(t *testing.T) {
})
require.NoError(t, err)

multipartBinaryEncodingCTUnsupported, multipartMimeBinaryEncodingCTUnsupported, err := newTestMultipartForm([]*testFormPart{
multipartBinaryEncodingCTFallback, multipartMimeBinaryEncodingCTFallback, err := newTestMultipartForm([]*testFormPart{
{name: "b", contentType: "application/json", data: strings.NewReader(`{"bar1": "bar1"}`), filename: "b1"},
{name: "d", contentType: "application/pdf", data: strings.NewReader("doo1"), filename: "d1"},
})
Expand All @@ -1688,6 +1688,12 @@ func TestDecodeBody(t *testing.T) {
})
require.NoError(t, err)

multipartBinaryEncodingCTList, multipartMimeBinaryEncodingCTList, err := newTestMultipartForm([]*testFormPart{
{name: "b", contentType: "text/plain", data: strings.NewReader("b1data"), filename: "b1"},
{name: "d", contentType: "application/json", data: strings.NewReader(`{"d1": "d1"}`), filename: "d1"},
})
require.NoError(t, err)

testCases := []struct {
name string
mime string
Expand Down Expand Up @@ -1839,25 +1845,17 @@ func TestDecodeBody(t *testing.T) {
want: map[string]any{"b": `{"bar1": "bar1"}`, "d": "doo1", "f": []any{`{"foo1": "foo1"}`, "foo2"}},
},
{
name: "multipartEncodingCTUnsupported",
mime: multipartMimeBinaryEncodingCTUnsupported,
body: multipartBinaryEncodingCTUnsupported,
name: "multipartEncodingCTBinaryFallback",
mime: multipartMimeBinaryEncodingCTFallback,
body: multipartBinaryEncodingCTFallback,
schema: openapi3.NewObjectSchema().
WithProperty("b", openapi3.NewStringSchema().WithFormat("binary")).
WithProperty("d", openapi3.NewStringSchema().WithFormat("binary")),
encoding: map[string]*openapi3.Encoding{
"b": {ContentType: "application/json"},
"d": {ContentType: "application/pdf"},
},
want: map[string]any{"b": map[string]any{"bar1": "bar1"}},
wantErr: &ParseError{
Kind: KindOther,
Cause: &ParseError{
Kind: KindUnsupportedFormat,
Reason: fmt.Sprintf("%s %q", prefixUnsupportedCT, "application/pdf"),
},
path: []any{"d"},
},
want: map[string]any{"b": map[string]any{"bar1": "bar1"}, "d": "doo1"},
},
{
name: "multipartEncodingCTNotMatching",
Expand Down Expand Up @@ -1885,6 +1883,19 @@ func TestDecodeBody(t *testing.T) {
path: []any{"d"},
},
},
{
name: "multipartEncodingCTList",
mime: multipartMimeBinaryEncodingCTList,
body: multipartBinaryEncodingCTList,
schema: openapi3.NewObjectSchema().
WithProperty("b", openapi3.NewStringSchema().WithFormat("binary")).
WithProperty("d", openapi3.NewStringSchema().WithFormat("binary")),
encoding: map[string]*openapi3.Encoding{
"b": {ContentType: "application/json, text/plain"},
"d": {ContentType: "application/*"},
},
want: map[string]any{"b": "b1data", "d": map[string]any{"d1": "d1"}},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -1914,6 +1925,33 @@ func TestDecodeBody(t *testing.T) {
}
}

func TestContentTypeAllowedByEncoding(t *testing.T) {
testCases := []struct {
name string
mediaType string
encodingContentType string
want bool
}{
{name: "exact match", mediaType: "application/json", encodingContentType: "application/json", want: true},
{name: "exact mismatch", mediaType: "application/json", encodingContentType: "application/xml", want: false},
{name: "comma list first", mediaType: "image/jpeg", encodingContentType: "image/jpeg, image/png", want: true},
{name: "comma list second", mediaType: "image/png", encodingContentType: "image/jpeg, image/png", want: true},
{name: "comma list miss", mediaType: "image/gif", encodingContentType: "image/jpeg, image/png", want: false},
{name: "comma list no spaces", mediaType: "image/png", encodingContentType: "image/jpeg,image/png", want: true},
{name: "subtype wildcard match", mediaType: "image/png", encodingContentType: "image/*", want: true},
{name: "subtype wildcard miss", mediaType: "application/pdf", encodingContentType: "image/*", want: false},
{name: "full wildcard", mediaType: "application/octet-stream", encodingContentType: "*/*", want: true},
{name: "case insensitive", mediaType: "Image/PNG", encodingContentType: "image/png", want: true},
{name: "entry with parameters base match", mediaType: "application/xml", encodingContentType: "application/xml; charset=utf-8", want: true},
{name: "empty entries ignored", mediaType: "image/png", encodingContentType: " , image/png , ", want: true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, contentTypeAllowedByEncoding(tc.mediaType, tc.encodingContentType))
})
}
}

type testFormPart struct {
name string
contentType string
Expand Down
Loading