Skip to content

Commit edb5d79

Browse files
authored
fix: restJson1 empty httpPayload body serialization (#402)
1 parent 28ee19a commit edb5d79

File tree

5 files changed

+55
-9
lines changed

5 files changed

+55
-9
lines changed

aws-runtime/http-client-engine-crt/common/src/aws/sdk/kotlin/runtime/http/engine/crt/RequestUtil.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ internal fun HttpRequest.toCrtRequest(callContext: CoroutineContext): aws.sdk.ko
4242
headers.forEach { key, values -> appendAll(key, values) }
4343
}
4444

45-
val contentLength = body.contentLength?.toString() ?: headers[CONTENT_LENGTH_HEADER]
45+
val bodyLen = body.contentLength
46+
val contentLength = when {
47+
bodyLen != null -> if (bodyLen > 0) bodyLen.toString() else null
48+
else -> headers[CONTENT_LENGTH_HEADER]
49+
}
4650
contentLength?.let { crtHeaders.append(CONTENT_LENGTH_HEADER, it) }
4751

4852
return aws.sdk.kotlin.crt.http.HttpRequest(method.name, url.encodedPath, crtHeaders.build(), bodyStream)

aws-runtime/http-client-engine-crt/common/test/aws/sdk/kotlin/runtime/http/engine/crt/RequestConversionTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,18 @@ class RequestConversionTest {
9191
val crtBody = crtRequest.body as ReadChannelBodyStream
9292
crtBody.cancel()
9393
}
94+
95+
@Test
96+
fun testEngineDoesNotAddContentLengthHeaderForEmptyBody() {
97+
val request = HttpRequest(
98+
HttpMethod.POST,
99+
Url.parse("https://test.aws.com?foo=bar"),
100+
Headers.Empty,
101+
HttpBody.Empty
102+
)
103+
104+
val testContext = EmptyCoroutineContext + Job()
105+
val crtRequest = request.toCrtRequest(testContext)
106+
assertFalse(crtRequest.headers.contains("Content-Length"))
107+
}
94108
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ package aws.sdk.kotlin.codegen.protocols
77
import aws.sdk.kotlin.codegen.protocols.core.AwsHttpBindingProtocolGenerator
88
import aws.sdk.kotlin.codegen.protocols.json.JsonHttpBindingProtocolGenerator
99
import software.amazon.smithy.aws.traits.protocols.RestJson1Trait
10+
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
11+
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
12+
import software.amazon.smithy.kotlin.codegen.core.defaultName
13+
import software.amazon.smithy.kotlin.codegen.core.withBlock
1014
import software.amazon.smithy.kotlin.codegen.rendering.protocol.*
1115
import software.amazon.smithy.model.Model
16+
import software.amazon.smithy.model.knowledge.HttpBinding
17+
import software.amazon.smithy.model.shapes.OperationShape
1218
import software.amazon.smithy.model.shapes.ServiceShape
1319
import software.amazon.smithy.model.shapes.ShapeId
20+
import software.amazon.smithy.model.shapes.StructureShape
1421

1522
/**
1623
* Handles generating the aws.protocols#restJson1 protocol for services.
@@ -24,4 +31,32 @@ class RestJson1 : JsonHttpBindingProtocolGenerator() {
2431

2532
override fun getProtocolHttpBindingResolver(model: Model, serviceShape: ServiceShape): HttpBindingResolver =
2633
HttpTraitResolver(model, serviceShape, "application/json")
34+
35+
override fun renderSerializeHttpBody(
36+
ctx: ProtocolGenerator.GenerationContext,
37+
op: OperationShape,
38+
writer: KotlinWriter
39+
) {
40+
super.renderSerializeHttpBody(ctx, op, writer)
41+
42+
val resolver = getProtocolHttpBindingResolver(ctx.model, ctx.service)
43+
if (!resolver.hasHttpBody(op)) return
44+
45+
// restjson1 has some different semantics and expectations around empty structures bound via @httpPayload trait
46+
// * empty structures get serialized to `{}`
47+
// see: https://github.com/awslabs/smithy/pull/924
48+
val requestBindings = resolver.requestBindings(op)
49+
val httpPayload = requestBindings.firstOrNull { it.location == HttpBinding.Location.PAYLOAD }
50+
if (httpPayload != null) {
51+
// explicit payload member as the sole payload
52+
val memberName = httpPayload.member.defaultName()
53+
val target = ctx.model.expectShape(httpPayload.member.target)
54+
if (target is StructureShape) {
55+
writer.withBlock("if (input.#L == null) {", "}", memberName) {
56+
addImport(RuntimeTypes.Http.ByteArrayContent)
57+
write("builder.body = #T(#S.encodeToByteArray())", RuntimeTypes.Http.ByteArrayContent, "{}")
58+
}
59+
}
60+
}
61+
}
2762
}

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,6 @@ abstract class AwsHttpBindingProtocolGenerator : HttpBindingProtocolGenerator()
7878
// FIXME - document type not fully supported yet, see https://github.com/awslabs/smithy-kotlin/issues/123
7979
"PutAndGetInlineDocumentsInput",
8080

81-
// aws-sdk-kotlin#390
82-
"RestJsonHttpWithHeaderMemberNoModeledBody",
83-
"RestJsonHttpWithNoModeledBody",
84-
"RestJsonHttpWithEmptyBlobPayload",
85-
"RestJsonHttpWithEmptyStructurePayload",
86-
"RestJsonHttpWithHeadersButNoPayload",
87-
8881
// smithy-kotlin#519
8982
"SimpleScalarPropertiesWithWhiteSpace",
9083
"SimpleScalarPropertiesPureWhiteSpace",

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ org.gradle.jvmargs=-Xmx6g -XX:MaxPermSize=6g -XX:MaxMetaspaceSize=1G
1010
sdkVersion=0.10.0-SNAPSHOT
1111

1212
# codegen
13-
smithyVersion=1.13.0
13+
smithyVersion=1.13.1
1414
smithyGradleVersion=0.5.3
1515
# smithy-kotlin codegen and runtime are versioned together
1616
smithyKotlinVersion=0.7.1-SNAPSHOT

0 commit comments

Comments
 (0)