Skip to content

Commit 74fb92d

Browse files
authored
fix: route53 custom error unmarshalling (#961)
1 parent 7efcd7b commit 74fb92d

File tree

7 files changed

+273
-0
lines changed

7 files changed

+273
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "7f80de87-a59a-420e-a175-c21a89f93fe9",
3+
"type": "bugfix",
4+
"description": "Correctly handle and throw `InvalidChangeBatch` responses from Route53",
5+
"issues": [
6+
"awslabs/aws-sdk-kotlin#242"
7+
]
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.codegen.customization.route53
6+
7+
import aws.sdk.kotlin.codegen.protocols.core.AwsHttpBindingProtocolGenerator
8+
import aws.sdk.kotlin.codegen.sdkId
9+
import software.amazon.smithy.kotlin.codegen.KotlinSettings
10+
import software.amazon.smithy.kotlin.codegen.core.withBlock
11+
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
12+
import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding
13+
import software.amazon.smithy.kotlin.codegen.model.expectShape
14+
import software.amazon.smithy.model.Model
15+
import software.amazon.smithy.model.shapes.ServiceShape
16+
17+
class ChangeResourceRecordSetsUnmarshallingIntegration : KotlinIntegration {
18+
override val sectionWriters: List<SectionWriterBinding> = listOf(
19+
SectionWriterBinding(AwsHttpBindingProtocolGenerator.ProtocolErrorDeserialization) { writer, _ ->
20+
writer.withBlock("payload?.let {", "}\n") {
21+
withBlock("aws.sdk.kotlin.services.route53.internal.parseRestXmlInvalidChangeBatchResponse(payload)?.let {", "}") {
22+
write("setAseErrorMetadata(it.exception, wrappedResponse, it.errorDetails)")
23+
write("throw it.exception")
24+
}
25+
}
26+
},
27+
)
28+
29+
override fun enabledForService(model: Model, settings: KotlinSettings) =
30+
model.expectShape<ServiceShape>(settings.service).sdkId.equals("route 53", ignoreCase = true)
31+
}

codegen/smithy-aws-kotlin-codegen/src/main/kotlin/aws/sdk/kotlin/codegen/protocols/core/AwsHttpBindingProtocolGenerator.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import aws.sdk.kotlin.codegen.protocols.protocoltest.AwsHttpProtocolUnitTestResp
1616
import software.amazon.smithy.aws.traits.protocols.AwsQueryCompatibleTrait
1717
import software.amazon.smithy.codegen.core.Symbol
1818
import software.amazon.smithy.kotlin.codegen.core.*
19+
import software.amazon.smithy.kotlin.codegen.integration.SectionId
1920
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
2021
import software.amazon.smithy.kotlin.codegen.model.buildSymbol
2122
import software.amazon.smithy.kotlin.codegen.model.hasTrait
@@ -99,6 +100,8 @@ abstract class AwsHttpBindingProtocolGenerator : HttpBindingProtocolGenerator()
99100
}
100101
}
101102

103+
object ProtocolErrorDeserialization : SectionId
104+
102105
protected open fun renderThrowOperationError(
103106
ctx: ProtocolGenerator.GenerationContext,
104107
op: OperationShape,
@@ -108,6 +111,7 @@ abstract class AwsHttpBindingProtocolGenerator : HttpBindingProtocolGenerator()
108111
writer.write("val payload = response.body.#T()", RuntimeTypes.Http.readAll)
109112
.write("val wrappedResponse = response.#T(payload)", RuntimeTypes.AwsProtocolCore.withPayload)
110113
.write("")
114+
.declareSection(ProtocolErrorDeserialization)
111115
.write("val errorDetails = try {")
112116
.indent()
113117
.call {

codegen/smithy-aws-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ aws.sdk.kotlin.codegen.customization.RemoveEventStreamOperations
2121
aws.sdk.kotlin.codegen.customization.flexiblechecksums.FlexibleChecksumsRequest
2222
aws.sdk.kotlin.codegen.customization.flexiblechecksums.FlexibleChecksumsResponse
2323
aws.sdk.kotlin.codegen.customization.route53.TrimResourcePrefix
24+
aws.sdk.kotlin.codegen.customization.route53.ChangeResourceRecordSetsUnmarshallingIntegration
2425
aws.sdk.kotlin.codegen.customization.s3.ClientConfigIntegration
2526
aws.sdk.kotlin.codegen.customization.s3.ContinueIntegration
2627
aws.sdk.kotlin.codegen.customization.s3.HttpPathFilter
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.codegen.customization.route53
6+
7+
import org.junit.jupiter.api.Test
8+
import software.amazon.smithy.kotlin.codegen.test.defaultSettings
9+
import software.amazon.smithy.kotlin.codegen.test.prependNamespaceAndService
10+
import software.amazon.smithy.kotlin.codegen.test.toSmithyModel
11+
import software.amazon.smithy.model.Model
12+
import kotlin.test.assertFalse
13+
import kotlin.test.assertTrue
14+
15+
class ChangeResourceRecordSetsUnmarshallingIntegrationTest {
16+
@Test
17+
fun nonRoute53ModelIntegration() {
18+
val model = sampleModel("not route 53")
19+
val isEnabledForModel = ChangeResourceRecordSetsUnmarshallingIntegration().enabledForService(model, model.defaultSettings())
20+
assertFalse(isEnabledForModel)
21+
}
22+
23+
@Test
24+
fun route53ModelIntegration() {
25+
val model = sampleModel("route 53")
26+
val isEnabledForModel = ChangeResourceRecordSetsUnmarshallingIntegration().enabledForService(model, model.defaultSettings())
27+
assertTrue(isEnabledForModel)
28+
}
29+
}
30+
31+
private fun sampleModel(serviceName: String): Model =
32+
"""
33+
@http(method: "PUT", uri: "/foo")
34+
operation Foo { }
35+
36+
@http(method: "POST", uri: "/bar")
37+
operation Bar { }
38+
"""
39+
.prependNamespaceAndService(operations = listOf("Foo", "Bar"), serviceName = serviceName)
40+
.toSmithyModel()
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.services.route53.internal
6+
7+
import aws.sdk.kotlin.services.route53.model.InvalidChangeBatch
8+
import aws.smithy.kotlin.runtime.awsprotocol.ErrorDetails
9+
import aws.smithy.kotlin.runtime.serde.*
10+
import aws.smithy.kotlin.runtime.serde.xml.*
11+
12+
internal fun parseRestXmlInvalidChangeBatchResponse(payload: ByteArray): InvalidChangeBatchErrorResponse? {
13+
return deserializeInvalidChangeBatchError(InvalidChangeBatch.Builder(), payload)
14+
}
15+
16+
internal fun deserializeInvalidChangeBatchError(builder: InvalidChangeBatch.Builder, payload: ByteArray): InvalidChangeBatchErrorResponse? {
17+
val deserializer = XmlDeserializer(payload)
18+
val MESSAGE_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, XmlSerialName("message"), XmlAliasName("Message"))
19+
val MESSAGES_DESCRIPTOR = SdkFieldDescriptor(SerialKind.List, XmlSerialName("messages"), XmlAliasName("Messages"), XmlCollectionName("Message"))
20+
val REQUESTID_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, XmlSerialName("RequestId"))
21+
val OBJ_DESCRIPTOR = SdkObjectDescriptor.build {
22+
trait(XmlSerialName("InvalidChangeBatch"))
23+
trait(XmlNamespace("https://route53.amazonaws.com/doc/2013-04-01/"))
24+
field(MESSAGE_DESCRIPTOR)
25+
field(MESSAGES_DESCRIPTOR)
26+
field(REQUESTID_DESCRIPTOR)
27+
}
28+
var requestId: String? = null
29+
30+
return try {
31+
deserializer.deserializeStruct(OBJ_DESCRIPTOR) {
32+
loop@while (true) {
33+
when (findNextFieldIndex()) {
34+
MESSAGE_DESCRIPTOR.index -> builder.message = deserializeString()
35+
REQUESTID_DESCRIPTOR.index -> requestId = deserializeString()
36+
MESSAGES_DESCRIPTOR.index ->
37+
builder.messages = deserializer.deserializeList(MESSAGES_DESCRIPTOR) {
38+
val col0 = mutableListOf<String>()
39+
while (hasNextElement()) {
40+
val el0 = if (nextHasValue()) { deserializeString() } else { deserializeNull(); continue }
41+
col0.add(el0)
42+
}
43+
col0
44+
}
45+
null -> break@loop
46+
else -> skipValue()
47+
}
48+
}
49+
}
50+
InvalidChangeBatchErrorResponse(ErrorDetails("InvalidChangeBatch", builder.message, requestId), builder.build())
51+
} catch (e: DeserializationException) {
52+
null // return so an appropriate exception type can be instantiated above here.
53+
}
54+
}
55+
56+
internal data class InvalidChangeBatchErrorResponse(
57+
val errorDetails: ErrorDetails,
58+
val exception: InvalidChangeBatch,
59+
)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.services.route53.internal
6+
7+
import aws.sdk.kotlin.services.route53.model.InvalidChangeBatch
8+
import aws.sdk.kotlin.services.route53.model.Route53Exception
9+
import aws.sdk.kotlin.services.route53.transform.ChangeResourceRecordSetsOperationDeserializer
10+
import aws.smithy.kotlin.runtime.http.Headers
11+
import aws.smithy.kotlin.runtime.http.HttpBody
12+
import aws.smithy.kotlin.runtime.http.HttpStatusCode
13+
import aws.smithy.kotlin.runtime.http.response.HttpResponse
14+
import aws.smithy.kotlin.runtime.operation.ExecutionContext
15+
import kotlinx.coroutines.runBlocking
16+
import org.junit.jupiter.api.Test
17+
import org.junit.jupiter.api.assertThrows
18+
import kotlin.test.assertEquals
19+
20+
class ChangeResourceRecordSetsUnmarshallingTest {
21+
@Test
22+
fun invalidChangeBatchMessage() {
23+
val bodyText = """
24+
<?xml version="1.0" encoding="UTF-8"?>
25+
<InvalidChangeBatch xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
26+
<Messages>
27+
<Message>InvalidChangeBatch message</Message>
28+
</Messages>
29+
<RequestId>b25f48e8-84fd-11e6-80d9-574e0c4664cb</RequestId>
30+
</InvalidChangeBatch>
31+
""".trimIndent()
32+
33+
val response: HttpResponse = HttpResponse(
34+
HttpStatusCode(400, "Bad Request"),
35+
Headers.invoke { },
36+
HttpBody.fromBytes(bodyText.encodeToByteArray()),
37+
)
38+
39+
val exception = assertThrows<InvalidChangeBatch> {
40+
runBlocking {
41+
ChangeResourceRecordSetsOperationDeserializer().deserialize(ExecutionContext(), response)
42+
}
43+
}
44+
assertEquals(listOf<String>("InvalidChangeBatch message"), exception.messages)
45+
}
46+
47+
@Test
48+
fun invalidChangeBatchMessage2() {
49+
val bodyText = """
50+
<?xml version="1.0" encoding="UTF-8"?>
51+
<InvalidChangeBatch xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
52+
<Messages>
53+
<Message>InvalidChangeBatch message 1</Message>
54+
<Message>InvalidChangeBatch message 2</Message>
55+
</Messages>
56+
<RequestId>b25f48e8-84fd-11e6-80d9-574e0c4664cb</RequestId>
57+
</InvalidChangeBatch>
58+
""".trimIndent()
59+
60+
val response: HttpResponse = HttpResponse(
61+
HttpStatusCode(400, "Bad Request"),
62+
Headers.invoke { },
63+
HttpBody.fromBytes(bodyText.encodeToByteArray()),
64+
)
65+
66+
val exception = assertThrows<InvalidChangeBatch> {
67+
runBlocking {
68+
ChangeResourceRecordSetsOperationDeserializer().deserialize(ExecutionContext(), response)
69+
}
70+
}
71+
assertEquals(listOf<String>("InvalidChangeBatch message 1", "InvalidChangeBatch message 2"), exception.messages)
72+
}
73+
74+
@Test
75+
fun invalidChangeBatchMessage3() {
76+
val bodyText = """
77+
<?xml version="1.0" encoding="UTF-8"?>
78+
<InvalidChangeBatch xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
79+
<Messages>
80+
<Message>InvalidChangeBatch message 1</Message>
81+
<Message>InvalidChangeBatch message 2</Message>
82+
</Messages>
83+
<Message>InvalidChangeBatch message 3</Message>
84+
<RequestId>b25f48e8-84fd-11e6-80d9-574e0c4664cb</RequestId>
85+
</InvalidChangeBatch>
86+
""".trimIndent()
87+
88+
val response: HttpResponse = HttpResponse(
89+
HttpStatusCode(400, "Bad Request"),
90+
Headers.invoke { },
91+
HttpBody.fromBytes(bodyText.encodeToByteArray()),
92+
)
93+
94+
val exception = assertThrows<InvalidChangeBatch> {
95+
runBlocking {
96+
ChangeResourceRecordSetsOperationDeserializer().deserialize(ExecutionContext(), response)
97+
}
98+
}
99+
assertEquals(listOf<String>("InvalidChangeBatch message 1", "InvalidChangeBatch message 2"), exception.messages)
100+
assertEquals("InvalidChangeBatch message 3", exception.message)
101+
}
102+
103+
@Test
104+
fun changeResourceRecordSetsError() {
105+
val bodyText = """
106+
<?xml version="1.0"?>
107+
<ErrorResponse xmlns="http://route53.amazonaws.com/doc/2016-09-07/">
108+
<Error>
109+
<Type>Sender</Type>
110+
<Code>MalformedXML</Code>
111+
<Message>ChangeResourceRecordSets error message</Message>
112+
</Error>
113+
<RequestId>b25f48e8-84fd-11e6-80d9-574e0c4664cb</RequestId>
114+
</ErrorResponse>
115+
""".trimIndent()
116+
117+
val response: HttpResponse = HttpResponse(
118+
HttpStatusCode(400, "Bad Request"),
119+
Headers.invoke { },
120+
HttpBody.fromBytes(bodyText.encodeToByteArray()),
121+
)
122+
123+
val exception = assertThrows<Route53Exception> {
124+
runBlocking {
125+
ChangeResourceRecordSetsOperationDeserializer().deserialize(ExecutionContext(), response)
126+
}
127+
}
128+
assertEquals("ChangeResourceRecordSets error message", exception.message)
129+
}
130+
}

0 commit comments

Comments
 (0)