Skip to content

Commit 372f815

Browse files
committed
feat(multipart-ct-decoder): set file decoder as default on binary and added validation on content-type from encoding
1 parent 2baea3d commit 372f815

File tree

3 files changed

+115
-4
lines changed

3 files changed

+115
-4
lines changed

openapi3filter/req_resp_decoder.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,7 +1211,10 @@ func UnregisterBodyDecoder(contentType string) {
12111211

12121212
var headerCT = http.CanonicalHeaderKey("Content-Type")
12131213

1214-
const prefixUnsupportedCT = "unsupported content type"
1214+
const (
1215+
prefixUnsupportedCT = "unsupported content type"
1216+
prefixNotMatchingCT = "not matching content types"
1217+
)
12151218

12161219
func isBinary(schema *openapi3.SchemaRef) bool {
12171220
if schema == nil || schema.Value == nil {
@@ -1220,6 +1223,20 @@ func isBinary(schema *openapi3.SchemaRef) bool {
12201223
return schema.Value.Type.Is("string") && schema.Value.Format == "binary"
12211224
}
12221225

1226+
func getEncodingContentType(encFn EncodingFn) string {
1227+
var enc *openapi3.Encoding
1228+
if encFn != nil {
1229+
// encFn is passed to decodeBody only in form body decoders as a subEncFn, so key can be ""
1230+
// func(string) *openapi3.Encoding { return enc }
1231+
enc = encFn("")
1232+
}
1233+
if enc == nil {
1234+
return ""
1235+
}
1236+
1237+
return enc.ContentType
1238+
}
1239+
12231240
// decodeBody returns a decoded body.
12241241
// The function returns ParseError when a body is invalid.
12251242
func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (
@@ -1235,11 +1252,26 @@ func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef,
12351252
}
12361253

12371254
mediaType := parseMediaType(contentType)
1238-
decoder, ok := bodyDecoders[mediaType]
1239-
if !ok && isBinary(schema) {
1240-
ok, decoder = true, FileBodyDecoder
1255+
encodingContentType := getEncodingContentType(encFn)
1256+
if isBinary(schema) && encodingContentType == "" {
1257+
value, err := FileBodyDecoder(body, header, schema, encFn)
1258+
return mediaType, value, err
12411259
}
12421260

1261+
if encodingContentType != "" &&
1262+
mediaType != encodingContentType {
1263+
return "", nil, &ParseError{
1264+
Kind: KindOther,
1265+
Reason: fmt.Sprintf(
1266+
"%s: header %q, encoding %q",
1267+
prefixNotMatchingCT,
1268+
mediaType,
1269+
encodingContentType,
1270+
),
1271+
}
1272+
}
1273+
1274+
decoder, ok := bodyDecoders[mediaType]
12431275
if !ok {
12441276
return "", nil, &ParseError{
12451277
Kind: KindUnsupportedFormat,

openapi3filter/req_resp_decoder_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1649,6 +1649,23 @@ func TestDecodeBody(t *testing.T) {
16491649
})
16501650
require.NoError(t, err)
16511651

1652+
multipartBinaryEncodingCT, multipartMimeBinaryEncodingCT, err := newTestMultipartForm([]*testFormPart{
1653+
{name: "b", contentType: "application/json", data: strings.NewReader(`{"bar1": "bar1"}`), filename: "b1"},
1654+
{name: "d", contentType: "application/pdf", data: strings.NewReader("doo1"), filename: "d1"},
1655+
{name: "f", contentType: "application/json", data: strings.NewReader(`{"foo1": "foo1"}`), filename: "f1"},
1656+
{name: "f", contentType: "application/pdf", data: strings.NewReader("foo2"), filename: "f2"},
1657+
})
1658+
1659+
multipartBinaryEncodingCTUnsupported, multipartMimeBinaryEncodingCTUnsupported, err := newTestMultipartForm([]*testFormPart{
1660+
{name: "b", contentType: "application/json", data: strings.NewReader(`{"bar1": "bar1"}`), filename: "b1"},
1661+
{name: "d", contentType: "application/pdf", data: strings.NewReader("doo1"), filename: "d1"},
1662+
})
1663+
1664+
multipartBinaryEncodingCTNotMatching, multipartMimeBinaryEncodingCTNotMatching, err := newTestMultipartForm([]*testFormPart{
1665+
{name: "b", contentType: "application/json", data: strings.NewReader(`{"bar1": "bar1"}`), filename: "b1"},
1666+
{name: "d", contentType: "application/pdf", data: strings.NewReader("doo1"), filename: "d1"},
1667+
})
1668+
16521669
testCases := []struct {
16531670
name string
16541671
mime string
@@ -1787,6 +1804,65 @@ func TestDecodeBody(t *testing.T) {
17871804
body: strings.NewReader("foo"),
17881805
want: "foo",
17891806
},
1807+
{
1808+
name: "multipartEncodingCT",
1809+
mime: multipartMimeBinaryEncodingCT,
1810+
body: multipartBinaryEncodingCT,
1811+
schema: openapi3.NewObjectSchema().
1812+
WithProperty("b", openapi3.NewStringSchema().WithFormat("binary")).
1813+
WithProperty("d", openapi3.NewStringSchema().WithFormat("binary")).
1814+
WithProperty("f", openapi3.NewArraySchema().WithItems(
1815+
openapi3.NewStringSchema().WithFormat("binary"),
1816+
)),
1817+
want: map[string]any{"b": `{"bar1": "bar1"}`, "d": "doo1", "f": []any{`{"foo1": "foo1"}`, "foo2"}},
1818+
},
1819+
{
1820+
name: "multipartEncodingCTUnsupported",
1821+
mime: multipartMimeBinaryEncodingCTUnsupported,
1822+
body: multipartBinaryEncodingCTUnsupported,
1823+
schema: openapi3.NewObjectSchema().
1824+
WithProperty("b", openapi3.NewStringSchema().WithFormat("binary")).
1825+
WithProperty("d", openapi3.NewStringSchema().WithFormat("binary")),
1826+
encoding: map[string]*openapi3.Encoding{
1827+
"b": {ContentType: "application/json"},
1828+
"d": {ContentType: "application/pdf"},
1829+
},
1830+
want: map[string]any{"b": map[string]any{"bar1": "bar1"}},
1831+
wantErr: &ParseError{
1832+
Kind: KindOther,
1833+
Cause: &ParseError{
1834+
Kind: KindUnsupportedFormat,
1835+
Reason: fmt.Sprintf("%s %q", prefixUnsupportedCT, "application/pdf"),
1836+
},
1837+
path: []any{"d"},
1838+
},
1839+
},
1840+
{
1841+
name: "multipartEncodingCTNotMatching",
1842+
mime: multipartMimeBinaryEncodingCTNotMatching,
1843+
body: multipartBinaryEncodingCTNotMatching,
1844+
schema: openapi3.NewObjectSchema().
1845+
WithProperty("b", openapi3.NewStringSchema().WithFormat("binary")).
1846+
WithProperty("d", openapi3.NewStringSchema().WithFormat("binary")),
1847+
encoding: map[string]*openapi3.Encoding{
1848+
"b": {ContentType: "application/json"},
1849+
"d": {ContentType: "application/test"},
1850+
},
1851+
want: map[string]any{"b": map[string]any{"bar1": "bar1"}},
1852+
wantErr: &ParseError{
1853+
Kind: KindOther,
1854+
Cause: &ParseError{
1855+
Kind: KindOther,
1856+
Reason: fmt.Sprintf(
1857+
"%s: header %q, encoding %q",
1858+
prefixNotMatchingCT,
1859+
"application/pdf",
1860+
"application/test",
1861+
),
1862+
},
1863+
path: []any{"d"},
1864+
},
1865+
},
17901866
}
17911867
for _, tc := range testCases {
17921868
t.Run(tc.name, func(t *testing.T) {

openapi3filter/zip_file_upload_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ paths:
4242
file:
4343
type: string
4444
format: binary
45+
encoding:
46+
file:
47+
contentType: application/zip
4548
responses:
4649
'200':
4750
description: Created

0 commit comments

Comments
 (0)