Skip to content

Commit a6f39fb

Browse files
committed
Fix request Content-Type header checking in servers
This fixes two bugs: 1. `Content-Type` header checking was succeeding when no `Content-Type` header was present but one was expected. 2. When a shape was @httpPayload`-bound, `Content-Type` header checking occurred even when no payload was being sent. In this case it is not necessary to check the header, since there is no content. Code has been refactored and cleaned up. The crux of the logic is now easier to understand, and contained in `content_type_header_classifier`.
1 parent 41b938d commit a6f39fb

File tree

10 files changed

+445
-171
lines changed

10 files changed

+445
-171
lines changed

CHANGELOG.next.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,16 @@
1010
# references = ["smithy-rs#920"]
1111
# meta = { "breaking" = false, "tada" = false, "bug" = false, "target" = "client | server | all"}
1212
# author = "rcoh"
13+
[[smithy-rs]]
14+
message = """Fix request `Content-Type` header checking
15+
16+
Two bugs related to how servers were checking the `Content-Type` header in incoming requests have been fixed:
17+
18+
1. `Content-Type` header checking was incorrectly succeeding when no `Content-Type` header was present but one was expected.
19+
2. When a shape was @httpPayload`-bound, `Content-Type` header checking occurred even when no payload was being sent. In this case it is not necessary to check the header, since there is no content.
20+
21+
This is a breaking change in that servers are now stricter at enforcing the expected `Content-Type` header is being sent by the client in general, and laxer when the shape is bound with `@httpPayload`.
22+
"""
23+
references = ["smithy-rs#3690"]
24+
meta = { "breaking" = true, "tada" = false, "bug" = true, "target" = "server"}
25+
author = "david-perez"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
$version: "1.0"
2+
3+
namespace aws.protocoltests.restjson
4+
5+
use aws.protocols#restJson1
6+
use smithy.test#httpMalformedRequestTests
7+
8+
@http(method: "POST", uri: "/MalformedContentTypeWithBody")
9+
operation MalformedContentTypeWithBody2 {
10+
input: GreetingStruct
11+
}
12+
13+
structure GreetingStruct {
14+
salutation: String,
15+
}
16+
17+
apply MalformedContentTypeWithBody2 @httpMalformedRequestTests([
18+
{
19+
id: "RestJsonWithBodyExpectsApplicationJsonContentTypeNoHeaders",
20+
documentation: """
21+
When there is modeled input, the content type must be application/json""",
22+
protocol: restJson1,
23+
request: {
24+
method: "POST",
25+
uri: "/MalformedContentTypeWithBody",
26+
body: "{}",
27+
},
28+
response: {
29+
code: 415,
30+
headers: {
31+
"x-amzn-errortype": "UnsupportedMediaTypeException"
32+
}
33+
},
34+
tags: [ "content-type" ]
35+
}
36+
])
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
$version: "1.0"
2+
3+
namespace aws.protocoltests.restjson
4+
5+
use aws.protocols#restJson1
6+
use smithy.test#httpMalformedRequestTests
7+
use smithy.test#httpRequestTests
8+
use smithy.test#httpResponseTests
9+
10+
/// This example serializes a blob shape in the payload.
11+
///
12+
/// In this example, no JSON document is synthesized because the payload is
13+
/// not a structure or a union type.
14+
@http(uri: "/HttpPayloadTraits", method: "POST")
15+
operation HttpPayloadTraits2 {
16+
input: HttpPayloadTraitsInputOutput,
17+
output: HttpPayloadTraitsInputOutput
18+
}
19+
20+
apply HttpPayloadTraits2 @httpRequestTests([
21+
{
22+
id: "RestJsonHttpPayloadTraitsWithBlobAcceptsNoContentType",
23+
documentation: """
24+
Servers must accept no content type for blob inputs
25+
without the media type trait.""",
26+
protocol: restJson1,
27+
method: "POST",
28+
uri: "/HttpPayloadTraits",
29+
body: "This is definitely a jpeg",
30+
bodyMediaType: "application/octet-stream",
31+
headers: {
32+
"X-Foo": "Foo",
33+
},
34+
params: {
35+
foo: "Foo",
36+
blob: "This is definitely a jpeg"
37+
},
38+
appliesTo: "server",
39+
tags: [ "content-type" ]
40+
}
41+
])
42+
43+
/// This example serializes a string shape in the payload.
44+
///
45+
/// In this example, no JSON document is synthesized because the payload is
46+
/// not a structure or a union type.
47+
@http(uri: "/HttpPayloadTraitOnString", method: "POST")
48+
operation HttpPayloadTraitOnString2 {
49+
input: HttpPayloadTraitOnStringInputOutput,
50+
output: HttpPayloadTraitOnStringInputOutput
51+
}
52+
53+
structure HttpPayloadTraitOnStringInputOutput {
54+
@httpPayload
55+
foo: String,
56+
}
57+
58+
apply HttpPayloadTraitOnString2 @httpRequestTests([
59+
{
60+
id: "RestJsonHttpPayloadTraitOnString",
61+
documentation: "Serializes a string in the HTTP payload",
62+
protocol: restJson1,
63+
method: "POST",
64+
uri: "/HttpPayloadTraitOnString",
65+
body: "Foo",
66+
bodyMediaType: "text/plain",
67+
headers: {
68+
"Content-Type": "text/plain",
69+
},
70+
requireHeaders: [
71+
"Content-Length"
72+
],
73+
params: {
74+
foo: "Foo",
75+
}
76+
},
77+
])
78+
79+
apply HttpPayloadTraitOnString2 @httpResponseTests([
80+
{
81+
id: "RestJsonHttpPayloadTraitOnString",
82+
documentation: "Serializes a string in the HTTP payload",
83+
protocol: restJson1,
84+
code: 200,
85+
body: "Foo",
86+
bodyMediaType: "text/plain",
87+
headers: {
88+
"Content-Type": "text/plain",
89+
},
90+
params: {
91+
foo: "Foo",
92+
}
93+
},
94+
])
95+
96+
apply HttpPayloadTraitOnString2 @httpMalformedRequestTests([
97+
{
98+
id: "RestJsonHttpPayloadTraitOnStringNoContentType",
99+
documentation: "Serializes a string in the HTTP payload without a content-type header",
100+
protocol: restJson1,
101+
request: {
102+
method: "POST",
103+
uri: "/HttpPayloadTraitOnString",
104+
body: "Foo",
105+
// We expect a `Content-Type` header but none was provided.
106+
},
107+
response: {
108+
code: 415,
109+
headers: {
110+
"x-amzn-errortype": "UnsupportedMediaTypeException"
111+
}
112+
},
113+
tags: [ "content-type" ]
114+
},
115+
{
116+
id: "RestJsonHttpPayloadTraitOnStringWrongContentType",
117+
documentation: "Serializes a string in the HTTP payload without the expected content-type header",
118+
protocol: restJson1,
119+
request: {
120+
method: "POST",
121+
uri: "/HttpPayloadTraitOnString",
122+
body: "Foo",
123+
headers: {
124+
// We expect `text/plain`.
125+
"Content-Type": "application/json",
126+
},
127+
},
128+
response: {
129+
code: 415,
130+
headers: {
131+
"x-amzn-errortype": "UnsupportedMediaTypeException"
132+
}
133+
},
134+
tags: [ "content-type" ]
135+
},
136+
{
137+
id: "RestJsonHttpPayloadTraitOnStringUnsatisfiableAccept",
138+
documentation: "Serializes a string in the HTTP payload with an unstatisfiable accept header",
139+
protocol: restJson1,
140+
request: {
141+
method: "POST",
142+
uri: "/HttpPayloadTraitOnString",
143+
body: "Foo",
144+
headers: {
145+
"Content-Type": "text/plain",
146+
// We can't satisfy this requirement; the server will return `text/plain`.
147+
"Accept": "application/json",
148+
},
149+
},
150+
response: {
151+
code: 406,
152+
headers: {
153+
"x-amzn-errortype": "NotAcceptableException"
154+
}
155+
},
156+
tags: [ "accept" ]
157+
},
158+
])
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
$version: "2.0"
2+
3+
namespace aws.protocoltests.restjson
4+
5+
use smithy.test#httpRequestTests
6+
use smithy.test#httpResponseTests
7+
use smithy.framework#ValidationException
8+
9+
@http(uri: "/EnumPayload2", method: "POST")
10+
@httpRequestTests([
11+
{
12+
id: "RestJsonEnumPayloadRequest2",
13+
uri: "/EnumPayload2",
14+
headers: { "Content-Type": "text/plain" },
15+
body: "enumvalue",
16+
params: { payload: "enumvalue" },
17+
method: "POST",
18+
protocol: "aws.protocols#restJson1"
19+
}
20+
])
21+
@httpResponseTests([
22+
{
23+
id: "RestJsonEnumPayloadResponse2",
24+
headers: { "Content-Type": "text/plain" },
25+
body: "enumvalue",
26+
params: { payload: "enumvalue" },
27+
protocol: "aws.protocols#restJson1",
28+
code: 200
29+
}
30+
])
31+
operation HttpEnumPayload2 {
32+
input: EnumPayloadInput,
33+
output: EnumPayloadInput
34+
errors: [ValidationException]
35+
}
36+
37+
@http(uri: "/StringPayload2", method: "POST")
38+
@httpRequestTests([
39+
{
40+
id: "RestJsonStringPayloadRequest2",
41+
uri: "/StringPayload2",
42+
headers: { "Content-Type": "text/plain" },
43+
body: "rawstring",
44+
params: { payload: "rawstring" },
45+
method: "POST",
46+
protocol: "aws.protocols#restJson1"
47+
}
48+
])
49+
@httpResponseTests([
50+
{
51+
id: "RestJsonStringPayloadResponse2",
52+
headers: { "Content-Type": "text/plain" },
53+
body: "rawstring",
54+
params: { payload: "rawstring" },
55+
protocol: "aws.protocols#restJson1",
56+
code: 200
57+
}
58+
])
59+
operation HttpStringPayload2 {
60+
input: StringPayloadInput,
61+
output: StringPayloadInput
62+
}

codegen-core/common-test-models/rest-json-extras.smithy

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ service RestJsonExtras {
6666
CaseInsensitiveErrorOperation,
6767
EmptyStructWithContentOnWireOp,
6868
QueryPrecedence,
69+
// TODO(https://github.com/smithy-lang/smithy/pull/2314)
70+
HttpPayloadTraitOnString2,
71+
HttpPayloadTraits2,
72+
// TODO(https://github.com/smithy-lang/smithy/pull/2310)
73+
MalformedContentTypeWithBody2,
74+
// TODO(https://github.com/smithy-lang/smithy/pull/2315)
75+
HttpEnumPayload2,
76+
HttpStringPayload2,
6977
],
7078
errors: [ExtraError]
7179
}
@@ -101,6 +109,7 @@ structure ExtraError {}
101109
id: "StringPayload",
102110
uri: "/StringPayload",
103111
body: "rawstring",
112+
headers: { "Content-Type": "text/plain" },
104113
params: { payload: "rawstring" },
105114
method: "POST",
106115
protocol: "aws.protocols#restJson1"

codegen-server-test/build.gradle.kts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,15 @@ val allCodegenTests = "../codegen-core/common-test-models".let { commonModels ->
6464
CodegenTest(
6565
"aws.protocoltests.restjson#RestJsonExtras",
6666
"rest_json_extras",
67-
imports = listOf("$commonModels/rest-json-extras.smithy"),
67+
imports = listOf(
68+
"$commonModels/rest-json-extras.smithy",
69+
// TODO(https://github.com/smithy-lang/smithy/pull/2310): Can be deleted when consumed in next Smithy version.
70+
"$commonModels/rest-json-extras-2310.smithy",
71+
// TODO(https://github.com/smithy-lang/smithy/pull/2314): Can be deleted when consumed in next Smithy version.
72+
"$commonModels/rest-json-extras-2314.smithy",
73+
// TODO(https://github.com/smithy-lang/smithy/pull/2315): Can be deleted when consumed in next Smithy version.
74+
"$commonModels/rest-json-extras-2315.smithy",
75+
),
6876
),
6977
CodegenTest(
7078
"aws.protocoltests.restjson.validation#RestJsonValidation",

codegen-server/src/main/kotlin/software/amazon/smithy/rust/codegen/server/smithy/generators/protocol/ServerProtocolTestGenerator.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,9 @@ class ServerProtocolTestGenerator(
812812
FailingTest(REST_JSON, "RestJsonEndpointTrait", TestType.Request),
813813
FailingTest(REST_JSON, "RestJsonEndpointTraitWithHostLabel", TestType.Request),
814814
FailingTest(REST_JSON, "RestJsonOmitsEmptyListQueryValues", TestType.Request),
815+
// TODO(https://github.com/smithy-lang/smithy/pull/2315): Can be deleted when fixed tests are consumed in next Smithy version
816+
FailingTest(REST_JSON, "RestJsonEnumPayloadRequest", TestType.Request),
817+
FailingTest(REST_JSON, "RestJsonStringPayloadRequest", TestType.Request),
815818
// Tests involving `@range` on floats.
816819
// Pending resolution from the Smithy team, see https://github.com/smithy-lang/smithy-rs/issues/2007.
817820
FailingTest(REST_JSON_VALIDATION, "RestJsonMalformedRangeFloat_case0", TestType.MalformedRequest),

0 commit comments

Comments
 (0)