1
0
mirror of https://github.com/square/okhttp.git synced 2025-07-29 17:41:17 +03:00

Don't leak response bodies in executeAsync (#8330)

* Don't leak response bodies in executeAsync

Also make callers opt in to an unstable coroutines API. If the resource
cleanup coroutines API changes, we'll have to change this API.

Remove the OkHttp experimental API. This is a good enough API as far
as OkHttp is concerned.

* Spotless
This commit is contained in:
Jesse Wilson
2024-04-05 07:25:23 -04:00
committed by GitHub
parent 4becb22672
commit aef791ac36
2 changed files with 72 additions and 3 deletions

View File

@ -20,10 +20,10 @@ package okhttp3
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.internal.closeQuietly
import okio.IOException
@OptIn(ExperimentalCoroutinesApi::class)
@ExperimentalOkHttpApi
@ExperimentalCoroutinesApi // resume with a resource cleanup.
suspend fun Call.executeAsync(): Response =
suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
@ -42,7 +42,9 @@ suspend fun Call.executeAsync(): Response =
call: Call,
response: Response,
) {
continuation.resume(value = response, onCancellation = { call.cancel() })
continuation.resume(response) {
response.closeQuietly()
}
}
},
)

View File

@ -26,15 +26,23 @@ import java.io.IOException
import java.util.concurrent.TimeUnit
import kotlin.test.assertFailsWith
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.job
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import mockwebserver3.MockResponse
import mockwebserver3.MockWebServer
import mockwebserver3.SocketPolicy.DisconnectAfterRequest
import okhttp3.HttpUrl.Companion.toHttpUrl
import okio.Buffer
import okio.ForwardingSource
import okio.buffer
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
@ -148,4 +156,63 @@ class SuspendCallTest {
}
}
}
@Test
fun responseClosedIfCoroutineCanceled() {
runTest {
val call = ClosableCall()
supervisorScope {
assertFailsWith<CancellationException> {
coroutineScope {
call.afterCallbackOnResponse = {
coroutineContext.job.cancel()
}
call.executeAsync()
}
}
}
assertThat(call.canceled).isTrue()
assertThat(call.responseClosed).isTrue()
}
}
/** A call that keeps track of whether its response body is closed. */
private class ClosableCall : FailingCall() {
private val response =
Response.Builder()
.request(Request("https://example.com/".toHttpUrl()))
.protocol(Protocol.HTTP_1_1)
.message("OK")
.code(200)
.body(
object : ResponseBody() {
override fun contentType() = null
override fun contentLength() = -1L
override fun source() =
object : ForwardingSource(Buffer()) {
override fun close() {
responseClosed = true
}
}.buffer()
},
)
.build()
var responseClosed = false
var canceled = false
var afterCallbackOnResponse: () -> Unit = {}
override fun cancel() {
canceled = true
}
override fun enqueue(responseCallback: Callback) {
responseCallback.onResponse(this, response)
afterCallbackOnResponse()
}
}
}