Skip to content

Commit 1e403e2

Browse files
authored
fix: set Content-Type header on empty bodies with Ktor if canonical request includes it (#630)
1 parent 59d0e4b commit 1e403e2

File tree

4 files changed

+42
-10
lines changed

4 files changed

+42
-10
lines changed

runtime/protocol/http-client-engines/http-client-engine-ktor/common/src/aws/smithy/kotlin/runtime/http/engine/ktor/KtorRequestAdapter.kt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package aws.smithy.kotlin.runtime.http.engine.ktor
77
import aws.smithy.kotlin.runtime.http.HttpBody
88
import aws.smithy.kotlin.runtime.http.request.HttpRequest
99
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
10-
import io.ktor.client.utils.EmptyContent
1110
import io.ktor.http.*
1211
import io.ktor.http.content.ByteArrayContent
1312
import io.ktor.http.content.OutgoingContent
@@ -45,18 +44,23 @@ internal class KtorRequestAdapter(
4544
// strip content type header which Ktor doesn't allow set this way for some reason
4645
val contentHeaders = builder.headers["Content-Type"]
4746
builder.headers.remove("Content-Type")
48-
val contentType: ContentType? = contentHeaders?.let { ContentType.parse(it) }
47+
val contentType: ContentType? = contentHeaders?.let(ContentType::parse)
4948

5049
// convert the request body
51-
when (val body = sdkRequest.body) {
52-
is HttpBody.Empty -> builder.body = EmptyContent
53-
is HttpBody.Bytes -> builder.body = ByteArrayContent(body.bytes(), contentType)
54-
is HttpBody.Streaming -> builder.body = proxyRequestStream(body, contentType)
50+
builder.body = when (val body = sdkRequest.body) {
51+
is HttpBody.Empty -> emptyContent(contentType)
52+
is HttpBody.Bytes -> ByteArrayContent(body.bytes(), contentType)
53+
is HttpBody.Streaming -> proxyRequestStream(body, contentType)
5554
}
5655

5756
return builder
5857
}
5958

59+
private fun emptyContent(contentType: ContentType?) = object : OutgoingContent.NoContent() {
60+
override val contentLength: Long = 0
61+
override val contentType = contentType
62+
}
63+
6064
@OptIn(ExperimentalStdlibApi::class)
6165
private fun proxyRequestStream(body: HttpBody.Streaming, contentType: ContentType?): OutgoingContent {
6266
val source = body.readFrom()

runtime/protocol/http-client-engines/http-client-engine-ktor/common/test/aws/smithy/kotlin/runtime/http/engine/ktor/KtorRequestAdapterTest.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ import io.ktor.utils.io.*
1616
import io.ktor.utils.io.core.*
1717
import kotlinx.coroutines.*
1818
import kotlinx.coroutines.test.runTest
19-
import kotlin.test.Test
20-
import kotlin.test.assertEquals
21-
import kotlin.test.assertTrue
19+
import kotlin.test.*
2220
import io.ktor.content.ByteArrayContent as KtorByteArrayContent
2321

2422
@OptIn(ExperimentalCoroutinesApi::class)
@@ -29,7 +27,11 @@ class KtorRequestAdapterTest {
2927
sdkBuilder.url { host = "test.aws.com" }
3028
sdkBuilder.header("Content-Type", "application/json")
3129
val actual = KtorRequestAdapter(sdkBuilder, coroutineContext).toBuilder()
32-
actual.headers.contains("Content-Type").shouldBeFalse()
30+
assertFalse("Content-Type" in actual.headers)
31+
32+
val body = actual.body
33+
assertIs<OutgoingContent.NoContent>(body)
34+
assertEquals(ContentType.parse("application/json"), body.contentType)
3335
}
3436

3537
@Test

runtime/protocol/http-client-engines/test-suite/common/test/aws/smithy/kotlin/runtime/http/test/UploadTest.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import aws.smithy.kotlin.runtime.http.HttpMethod
1111
import aws.smithy.kotlin.runtime.http.HttpStatusCode
1212
import aws.smithy.kotlin.runtime.http.content.ByteArrayContent
1313
import aws.smithy.kotlin.runtime.http.request.HttpRequest
14+
import aws.smithy.kotlin.runtime.http.request.headers
1415
import aws.smithy.kotlin.runtime.http.request.url
1516
import aws.smithy.kotlin.runtime.http.response.complete
1617
import aws.smithy.kotlin.runtime.http.test.util.AbstractEngineTest
@@ -112,4 +113,28 @@ class UploadTest : AbstractEngineTest() {
112113
assertEquals(sha, call.response.headers["content-sha256"], "sha mismatch for upload on ${client.engine}")
113114
}
114115
}
116+
117+
@Test
118+
fun testUploadEmptyWithContentTypes() = testEngines {
119+
// test that empty bodies with a specified Content-Type actually include Content-Type in the request
120+
// see https://github.com/awslabs/aws-sdk-kotlin/issues/588
121+
test { env, client ->
122+
val req = HttpRequest {
123+
method = HttpMethod.POST
124+
headers {
125+
append("Content-Type", "application/xml")
126+
}
127+
url(env.testServer)
128+
url.path = "/upload/content"
129+
body = HttpBody.Empty
130+
}
131+
132+
val call = client.call(req)
133+
call.complete()
134+
assertEquals(HttpStatusCode.OK, call.response.status)
135+
136+
val reqContentType = call.response.headers["request-content-type"]
137+
assertEquals("application/xml", reqContentType, "No Content-Type set on ${client.engine}")
138+
}
139+
}
115140
}

runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/suite/Uploads.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ internal fun Application.uploadTests() {
3131
}
3232

3333
call.response.header("content-sha256", contentSha.digest().encodeToHex())
34+
call.request.headers["Content-Type"]?.let { call.response.header("request-content-type", it) }
3435
call.respond(HttpStatusCode.OK)
3536
}
3637
}

0 commit comments

Comments
 (0)