Skip to content

Commit 99ae92c

Browse files
authored
fix(rt): okhttp engine crashing on Android when coroutine is cancelled while uploading request body (#723)
1 parent 5fb51db commit 99ae92c

File tree

4 files changed

+66
-5
lines changed

4 files changed

+66
-5
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "841987e7-c68c-4ff6-8426-f7bf51373d79",
3+
"type": "bugfix",
4+
"description": "Fix OkHttp engine crashing on Android when coroutine is cancelled while uploading request body",
5+
"issues": [
6+
"awslabs/aws-sdk-kotlin#733"
7+
]
8+
}

runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/ByteChannelRequestBody.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import kotlinx.coroutines.*
1010
import okhttp3.MediaType
1111
import okhttp3.RequestBody
1212
import okio.BufferedSink
13+
import java.io.IOException
1314
import java.nio.ByteBuffer
1415
import kotlin.coroutines.CoroutineContext
1516

@@ -76,7 +77,9 @@ private inline fun <T> withJob(job: CompletableJob, block: () -> T): T {
7677
return block()
7778
} catch (ex: Exception) {
7879
job.completeExceptionally(ex)
79-
throw ex
80+
// wrap all exceptions thrown from inside `okhttp3.RequestBody#writeTo(..)` as an IOException
81+
// see https://github.com/awslabs/aws-sdk-kotlin/issues/733
82+
throw IOException(ex)
8083
} finally {
8184
job.complete()
8285
}

runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/test/aws/smithy/kotlin/runtime/http/engine/okhttp/ByteChannelRequestBodyTest.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,9 @@ import kotlinx.coroutines.test.runTest
1515
import okio.Buffer
1616
import okio.BufferedSink
1717
import org.junit.jupiter.api.Test
18+
import java.io.IOException
1819
import kotlin.coroutines.EmptyCoroutineContext
19-
import kotlin.test.assertEquals
20-
import kotlin.test.assertFalse
21-
import kotlin.test.assertTrue
20+
import kotlin.test.*
2221
import kotlin.time.Duration.Companion.milliseconds
2322
import kotlin.time.Duration.Companion.seconds
2423

@@ -106,11 +105,17 @@ class ByteChannelRequestBodyTest {
106105
override val contentLength: Long = content.size.toLong()
107106
override fun readFrom(): SdkByteReadChannel = chan
108107
}
108+
109109
val job = launch(Dispatchers.IO) {
110110
val callContext = coroutineContext + Job(coroutineContext.job)
111111
val actual = ByteChannelRequestBody(body, callContext)
112112
val buffer = Buffer()
113-
actual.writeTo(buffer)
113+
// see https://github.com/awslabs/aws-sdk-kotlin/issues/733 for why we expect
114+
// this to be an IOException
115+
val ex = assertFailsWith<IOException> {
116+
actual.writeTo(buffer)
117+
}
118+
assertIs<CancellationException>(ex.cause)
114119
}
115120
delay(100.milliseconds)
116121

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,49 @@ class UploadTest : AbstractEngineTest() {
165165
assertEquals("application/xml", reqContentType, "No Content-Type set on ${client.engine}")
166166
}
167167
}
168+
169+
/**
170+
* Test cancelling the request in the middle of processing the request body
171+
* This recreates [aws-sdk-kotlin#733](https://github.com/awslabs/aws-sdk-kotlin/issues/733)
172+
* but does not result in a test failure (instead you will see
173+
* 'Exception in thread "OkHttp Dispatcher" ... ' in the test output if not handled).
174+
*
175+
* If we exposed configuring the `Dispatcher` on OkHttpEngine builder then we could
176+
* plug in a custom executor/thread factory to make the actual assertions (in a JVM test sourceSet).
177+
* Although that would only work so long as okhttp continues to use `execute` rather than `submit` internally.
178+
*/
179+
@OptIn(DelicateCoroutinesApi::class)
180+
@Test
181+
fun testUploadCancellation() = testEngines {
182+
test { env, client ->
183+
val chan = SdkByteChannel(autoFlush = true)
184+
185+
val writeJob = GlobalScope.launch {
186+
val seedData = ByteArray(6017) { it.toByte() }
187+
chan.writeFully(seedData)
188+
}
189+
190+
val req = HttpRequest {
191+
method = HttpMethod.POST
192+
testSetup(env)
193+
url.path = "/upload/content"
194+
body = chan.toHttpBody(16 * 1024 * 1024)
195+
}
196+
197+
val job = GlobalScope.launch {
198+
val call = client.call(req)
199+
try {
200+
// wait for seed data to have been written
201+
writeJob.join()
202+
delay(5000)
203+
} finally {
204+
call.complete()
205+
chan.cancel(null)
206+
}
207+
}
208+
209+
writeJob.join()
210+
job.cancelAndJoin()
211+
}
212+
}
168213
}

0 commit comments

Comments
 (0)