Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import kotlinx.cinterop.UnsafeNumber
import kotlinx.coroutines.CompletableDeferred
import platform.Foundation.*
import platform.darwin.NSObject
import kotlin.collections.set
import kotlin.coroutines.CoroutineContext

private const val HTTP_REQUESTS_INITIAL_CAPACITY = 32
Expand All @@ -37,6 +36,7 @@ public fun KtorNSURLSessionDelegate(): KtorNSURLSessionDelegate {
*
* For HTTP requests to work property, it's important that users call these functions:
* * URLSession:dataTask:didReceiveData:
* * URLSession:task:didFinishCollectingMetrics:
* * URLSession:task:didCompleteWithError:
* * URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:
*
Expand Down Expand Up @@ -65,6 +65,16 @@ public class KtorNSURLSessionDelegate(
override fun URLSession(session: NSURLSession, taskIsWaitingForConnectivity: NSURLSessionTask) {
}

override fun URLSession(
session: NSURLSession,
task: NSURLSessionTask,
didFinishCollectingMetrics: NSURLSessionTaskMetrics
) {
val lastTransactionMetrics = didFinishCollectingMetrics.transactionMetrics.lastOrNull()
as? NSURLSessionTaskTransactionMetrics
lastTransactionMetrics?.let { taskHandlers[task]?.saveMetrics(it) }
}

override fun URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError: NSError?) {
taskHandlers[task]?.let {
it.complete(task, didCompleteWithError)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.engine.darwin.internal
Expand All @@ -9,12 +9,16 @@ import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.util.date.*
import io.ktor.utils.io.*
import io.ktor.utils.io.CancellationException
import kotlinx.cinterop.*
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.UnsafeNumber
import kotlinx.cinterop.convert
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import platform.Foundation.*
import kotlin.coroutines.*
import kotlin.coroutines.CoroutineContext

@OptIn(DelicateCoroutinesApi::class)
internal class DarwinTaskHandler(
Expand All @@ -29,6 +33,8 @@ internal class DarwinTaskHandler(
private var pendingFailure: Throwable? = null
get() = field?.also { field = null }

private var metrics: NSURLSessionTaskTransactionMetrics? = null

private val body: ByteReadChannel = GlobalScope.writer(callContext) {
try {
bodyChunks.consumeEach {
Expand All @@ -42,15 +48,10 @@ internal class DarwinTaskHandler(
}.channel

fun receiveData(dataTask: NSURLSessionDataTask, data: NSData) {
if (!response.isCompleted) {
val result = dataTask.response as NSHTTPURLResponse
response.complete(result.toResponseData(requestData))
}
Copy link
Member Author

@osipxd osipxd Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually wrong. Events are called in the following order:
:didReceiveData:didFinishCollectingMetrics:didCompleteWithError

:didFinishCollectingMetrics is the place where we get the protocol version. So I thought we can complete response only from :didCompleteWithError. The problem is that this event is not called until we've completely read the body. And it is a problem for streaming responses. That's why testDownloadStreamResponseWithCancel hangs.

I'll try to find a different solution.


val content = data.toByteArray()
try {
bodyChunks.trySend(content).isSuccess
} catch (cause: CancellationException) {
} catch (_: CancellationException) {
dataTask.cancel()
}
}
Expand All @@ -59,6 +60,10 @@ internal class DarwinTaskHandler(
pendingFailure = cause
}

fun saveMetrics(taskMetrics: NSURLSessionTaskTransactionMetrics) {
metrics = taskMetrics
}

fun complete(task: NSURLSessionTask, didCompleteWithError: NSError?) {
if (didCompleteWithError != null) {
val exception = pendingFailure ?: handleNSError(requestData, didCompleteWithError)
Expand Down Expand Up @@ -87,9 +92,16 @@ internal class DarwinTaskHandler(
status,
requestTime,
headers,
HttpProtocolVersion.HTTP_1_1,
protocolVersion(),
responseBody,
callContext
)
}

private fun protocolVersion(): HttpProtocolVersion = when (metrics?.networkProtocolName) {
"http/1.1" -> HttpProtocolVersion.HTTP_1_1
"h2", "h2c" -> HttpProtocolVersion.HTTP_2_0
"h3" -> HttpProtocolVersion.HTTP_3_0
else -> HttpProtocolVersion.HTTP_1_1
}
}
6 changes: 0 additions & 6 deletions ktor-client/ktor-client-darwin/darwin/test/DarwinHttp2Test.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,10 @@ package io.ktor.client.engine.darwin
import io.ktor.client.engine.darwin.utils.*
import io.ktor.client.tests.*
import kotlinx.cinterop.UnsafeNumber
import kotlin.test.Ignore
import kotlin.test.Test

class DarwinHttp2Test : Http2Test<DarwinClientEngineConfig>(Darwin, useH2c = false) {
@OptIn(UnsafeNumber::class)
override fun DarwinClientEngineConfig.disableCertificateValidation() {
handleChallenge { _, _, challenge, completionHandler -> trustAnyCertificate(challenge, completionHandler) }
}

@Ignore // KTOR-9095 Darwin: HttpResponse.version always returns HTTP_1_1
@Test
override fun `test protocol version is HTTP 2`() {}
}