Skip to content

Commit 4b66264

Browse files
authored
Send x-amzn-query-mode to inform a service with the awsQueryCompatible trait that SDK is operating in that mode (#3883)
## Motivation and Context If a client SDK is generated from a service model that has the [`awsQueryCompatible`](https://smithy.io/2.0/aws/protocols/aws-query-protocol.html#aws-protocols-awsquerycompatible-trait) trait, the SDK now sends `x-amzn-query-mode` in the request header for the service. ## Description The change in the PR itself is pretty simple, as said in the title. It's more important to understand why we are making these changes. The rest of the description will focus on the reason driving this change. The `awsQueryCompatible` trait, added by a service, is specifically for deserializing errors. It allows for deserializing errors in a backward compatible manner when the service migrates away from the AWS Query protocol. With [the awsQueryError trait](https://smithy.io/2.0/aws/protocols/aws-query-protocol.html#aws-protocols-awsqueryerror-trait), the AWS Query supports customizing error codes that is not supported in any other AWS protocol, e.g. ``` @awsQueryError( code: "AWS.SimpleQueueService.NonExistentQueue", httpResponseCode: 400 ) @error("client") structure QueueDoesNotExistException { message: String } ``` In short, the `awsQueryCompatible` trait makes it possible to continue using the custom error codes even when the service drops support for the AWS Query protocol and switches to other protocols such as `awsJson1_0` and `rpcv2Cbor` (see [example snippet](https://smithy.io/2.0/aws/protocols/aws-query-protocol.html#aws-protocols-awsquerycompatible-trait) in the Smithy documentation) The changes in this PR would be unnecessary if a service had originally supported only `@awsQuery` and had _atomically_ updated its Smithy model to replace `@awsQuery` with `@awsQueryCompatible` and `@awsJson1_0` in lockstep. However, that's not always the case in practice. Consider a service whose Smithy model supports two protocols: `@awsQuery` and `@awsJson1_0`. While the Rust SDK maintains [an ordered map of protocols](https://github.com/smithy-lang/smithy-rs/blob/de4bc4547df15e2472b9bca91c89f963ffad0b03/codegen-client/src/main/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/ClientProtocolLoader.kt#L38-L47) to determine which one to use, it’s possible for two groups of client SDKs to be generated over time, depending on which protocol was added first to the service: 1. Client SDKs that understand the `awsQuery` protocol (this group can interpret custom error codes sent in responses from the service) 2. Client SDKs that understand the `awsJson1_0` protocol (this group does not interpret custom error codes) Now, imagine if the service updated its Smithy model to remove `@awsQuery` and add `@awsQueryCompatible` (likely it would add the `awsQueryError` trait to an error structure somewhere as well). The supported protocols would then be `@awsJson1_0` and `@awsQueryCompatible`. Group 1 remains unaffected, as they can continue deserializing responses with custom error codes. However, group 2 would now be broken, as they would begin receiving custom error codes in responses due to the `awsQueryCompatible` trait. To prevent the issue for group 2 above, the `x-amzn-query-mode` header in this PR informs the service that it should only send back custom error codes if the client SDK is built with the `awsQueryCompatible` trait. With this update, client SDKs built only with the `awsJson1_0` will remain unaffected, as they do not send the `x-amzn-query-mode` header to the service and, therefore, will not receive custom error codes in response. ## Testing - Added a Kotlin test to check for the `x-amzn-query-mode` header - Added `x-amzn-query-mode` as a required header to a protocol test for the `awsQueryCompatible` ## Checklist - [x] For changes to the AWS SDK, generated SDK code, or SDK runtime crates, I have created a changelog entry Markdown file in the `.changelog` directory, specifying "aws-sdk-rust" in the `applies_to` key. ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent de4bc45 commit 4b66264

File tree

4 files changed

+119
-89
lines changed

4 files changed

+119
-89
lines changed

.changelog/1729271936.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
applies_to:
3+
- aws-sdk-rust
4+
authors:
5+
- ysaito1001
6+
references:
7+
- smithy-rs#3883
8+
breaking: false
9+
new_feature: false
10+
bug_fix: false
11+
---
12+
Client SDKs built with the `awsQueryCompatible` trait now include the `x-amzn-query-mode` header. This header signals the service that the clients are operating in compatible mode.

codegen-client/src/test/kotlin/software/amazon/smithy/rust/codegen/client/smithy/protocols/AwsQueryCompatibleTest.kt

Lines changed: 99 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,29 @@
66
package software.amazon.smithy.rust.codegen.client.smithy.protocols
77

88
import org.junit.jupiter.api.Test
9-
import software.amazon.smithy.model.shapes.OperationShape
109
import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest
1110
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency
1211
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
12+
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
1313
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
14-
import software.amazon.smithy.rust.codegen.core.util.lookup
14+
import software.amazon.smithy.rust.codegen.core.testutil.testModule
15+
import software.amazon.smithy.rust.codegen.core.testutil.tokioTest
16+
import software.amazon.smithy.rust.codegen.core.util.letIf
1517

1618
class AwsQueryCompatibleTest {
17-
@Test
18-
fun `aws-query-compatible json with aws query error should allow for retrieving error code and type from custom header`() {
19-
val model =
20-
"""
19+
companion object {
20+
const val prologue = """
2121
namespace test
2222
use aws.protocols#awsJson1_0
2323
use aws.protocols#awsQueryCompatible
2424
use aws.protocols#awsQueryError
25+
"""
2526

26-
@awsQueryCompatible
27-
@awsJson1_0
27+
const val awsjson10Trait = "@awsJson1_0"
28+
const val awsQueryCompatibleTrait = "@awsQueryCompatible"
29+
30+
fun testService(withAwsQueryError: Boolean = true) =
31+
"""
2832
service TestService {
2933
version: "2023-02-20",
3034
operations: [SomeOperation]
@@ -40,36 +44,45 @@ class AwsQueryCompatibleTest {
4044
a: String,
4145
b: Integer
4246
}
43-
44-
@awsQueryError(
45-
code: "InvalidThing",
46-
httpResponseCode: 400,
47-
)
48-
@error("client")
49-
structure InvalidThingException {
50-
message: String
47+
""".letIf(withAwsQueryError) {
48+
it +
49+
"""
50+
@awsQueryError(
51+
code: "InvalidThing",
52+
httpResponseCode: 400,
53+
)
54+
"""
55+
}.let {
56+
it +
57+
"""
58+
@error("client")
59+
structure InvalidThingException {
60+
message: String
61+
}
62+
"""
5163
}
52-
""".asSmithyModel()
64+
}
5365

66+
@Test
67+
fun `aws-query-compatible json with aws query error should allow for retrieving error code and type from custom header`() {
68+
val model =
69+
(prologue + awsQueryCompatibleTrait + awsjson10Trait + testService()).asSmithyModel(
70+
smithyVersion = "2",
71+
)
5472
clientIntegrationTest(model) { context, rustCrate ->
55-
val operation: OperationShape = context.model.lookup("test#SomeOperation")
56-
rustCrate.withModule(context.symbolProvider.moduleForShape(operation)) {
57-
rustTemplate(
58-
"""
59-
##[cfg(test)]
60-
##[#{tokio}::test]
61-
async fn should_parse_code_and_type_fields() {
62-
use aws_smithy_types::body::SdkBody;
63-
64-
let response = |_: http::Request<SdkBody>| {
73+
rustCrate.testModule {
74+
tokioTest("should_parse_code_and_type_fields") {
75+
rustTemplate(
76+
"""
77+
let response = |_: http::Request<#{SdkBody}>| {
6578
http::Response::builder()
6679
.header(
6780
"x-amzn-query-error",
6881
http::HeaderValue::from_static("AWS.SimpleQueueService.NonExistentQueue;Sender"),
6982
)
7083
.status(400)
7184
.body(
72-
SdkBody::from(
85+
#{SdkBody}::from(
7386
r##"{
7487
"__type": "com.amazonaws.sqs##QueueDoesNotExist",
7588
"message": "Some user-visible message"
@@ -86,68 +99,38 @@ class AwsQueryCompatibleTest {
8699
);
87100
let error = dbg!(client.some_operation().send().await).err().unwrap().into_service_error();
88101
assert_eq!(
89-
Some("AWS.SimpleQueueService.NonExistentQueue"),
102+
#{Some}("AWS.SimpleQueueService.NonExistentQueue"),
90103
error.meta().code(),
91104
);
92-
assert_eq!(Some("Sender"), error.meta().extra("type"));
93-
}
94-
""",
95-
"infallible_client_fn" to
96-
CargoDependency.smithyRuntimeTestUtil(context.runtimeConfig)
97-
.toType().resolve("client::http::test_util::infallible_client_fn"),
98-
"tokio" to CargoDependency.Tokio.toType(),
99-
)
105+
assert_eq!(#{Some}("Sender"), error.meta().extra("type"));
106+
""",
107+
*RuntimeType.preludeScope,
108+
"SdkBody" to RuntimeType.sdkBody(context.runtimeConfig),
109+
"infallible_client_fn" to
110+
CargoDependency.smithyRuntimeTestUtil(context.runtimeConfig)
111+
.toType().resolve("client::http::test_util::infallible_client_fn"),
112+
)
113+
}
100114
}
101115
}
102116
}
103117

104118
@Test
105119
fun `aws-query-compatible json without aws query error should allow for retrieving error code from payload`() {
106120
val model =
107-
"""
108-
namespace test
109-
use aws.protocols#awsJson1_0
110-
use aws.protocols#awsQueryCompatible
111-
112-
@awsQueryCompatible
113-
@awsJson1_0
114-
service TestService {
115-
version: "2023-02-20",
116-
operations: [SomeOperation]
117-
}
118-
119-
operation SomeOperation {
120-
input: SomeOperationInputOutput,
121-
output: SomeOperationInputOutput,
122-
errors: [InvalidThingException],
123-
}
124-
125-
structure SomeOperationInputOutput {
126-
a: String,
127-
b: Integer
128-
}
129-
130-
@error("client")
131-
structure InvalidThingException {
132-
message: String
133-
}
134-
""".asSmithyModel()
135-
121+
(prologue + awsQueryCompatibleTrait + awsjson10Trait + testService(withAwsQueryError = false)).asSmithyModel(
122+
smithyVersion = "2",
123+
)
136124
clientIntegrationTest(model) { context, rustCrate ->
137-
val operation: OperationShape = context.model.lookup("test#SomeOperation")
138-
rustCrate.withModule(context.symbolProvider.moduleForShape(operation)) {
139-
rustTemplate(
140-
"""
141-
##[cfg(test)]
142-
##[#{tokio}::test]
143-
async fn should_parse_code_from_payload() {
144-
use aws_smithy_types::body::SdkBody;
145-
146-
let response = |_: http::Request<SdkBody>| {
125+
rustCrate.testModule {
126+
tokioTest("should_parse_code_from_payload") {
127+
rustTemplate(
128+
"""
129+
let response = |_: http::Request<#{SdkBody}>| {
147130
http::Response::builder()
148131
.status(400)
149132
.body(
150-
SdkBody::from(
133+
#{SdkBody}::from(
151134
r##"{
152135
"__type": "com.amazonaws.sqs##QueueDoesNotExist",
153136
"message": "Some user-visible message"
@@ -163,15 +146,45 @@ class AwsQueryCompatibleTest {
163146
.build()
164147
);
165148
let error = dbg!(client.some_operation().send().await).err().unwrap().into_service_error();
166-
assert_eq!(Some("QueueDoesNotExist"), error.meta().code());
167-
assert_eq!(None, error.meta().extra("type"));
168-
}
169-
""",
170-
"infallible_client_fn" to
171-
CargoDependency.smithyRuntimeTestUtil(context.runtimeConfig)
172-
.toType().resolve("client::http::test_util::infallible_client_fn"),
173-
"tokio" to CargoDependency.Tokio.toType(),
174-
)
149+
assert_eq!(#{Some}("QueueDoesNotExist"), error.meta().code());
150+
assert_eq!(#{None}, error.meta().extra("type"));
151+
""",
152+
*RuntimeType.preludeScope,
153+
"SdkBody" to RuntimeType.sdkBody(context.runtimeConfig),
154+
"infallible_client_fn" to
155+
CargoDependency.smithyRuntimeTestUtil(context.runtimeConfig)
156+
.toType().resolve("client::http::test_util::infallible_client_fn"),
157+
)
158+
}
159+
}
160+
}
161+
}
162+
163+
@Test
164+
fun `request header should include x-amzn-query-mode when the service has the awsQueryCompatible trait`() {
165+
val model =
166+
(prologue + awsQueryCompatibleTrait + awsjson10Trait + testService()).asSmithyModel(
167+
smithyVersion = "2",
168+
)
169+
clientIntegrationTest(model) { context, rustCrate ->
170+
rustCrate.testModule {
171+
tokioTest("test_request_header_should_include_x_amzn_query_mode") {
172+
rustTemplate(
173+
"""
174+
let (http_client, rx) = #{capture_request}(#{None});
175+
let config = crate::Config::builder()
176+
.http_client(http_client)
177+
.endpoint_url("http://localhost:1234/SomeOperation")
178+
.build();
179+
let client = crate::Client::from_conf(config);
180+
let _ = dbg!(client.some_operation().send().await);
181+
let request = rx.expect_request();
182+
assert_eq!("true", request.headers().get("x-amzn-query-mode").unwrap());
183+
""",
184+
*RuntimeType.preludeScope,
185+
"capture_request" to RuntimeType.captureRequest(context.runtimeConfig),
186+
)
187+
}
175188
}
176189
}
177190
}

codegen-core/common-test-models/aws-json-query-compat.smithy

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@ service QueryCompatService {
2424
params: {
2525
message: "hello!"
2626
},
27-
headers: { "x-amz-target": "QueryCompatService.Operation"}
28-
27+
headers: {
28+
"x-amz-target": "QueryCompatService.Operation",
29+
"x-amzn-query-mode": "true",
30+
}
2931
}
3032
])
3133
operation Operation {

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/smithy/protocols/AwsQueryCompatible.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,5 +97,8 @@ class AwsQueryCompatible(
9797
awsJson.parseEventStreamErrorMetadata(operationShape)
9898

9999
override fun additionalRequestHeaders(operationShape: OperationShape): List<Pair<String, String>> =
100-
listOf("x-amz-target" to "${codegenContext.serviceShape.id.name}.${operationShape.id.name}")
100+
listOf(
101+
"x-amz-target" to "${codegenContext.serviceShape.id.name}.${operationShape.id.name}",
102+
"x-amzn-query-mode" to "true",
103+
)
101104
}

0 commit comments

Comments
 (0)