Repository navigation
Exception during streaming: Flow invariant is violated in KtorKoogHttpClient.lines #2245
Copy link
Copy link
Open
Description
Bug
LLMClient is used that requires a KtorKoogHttpClient (for example, OllamaClient) in my KMP application. Everything is fine on android target but on IOS there's an exception that is happening during the agent run
Stacktrace
ai.koog.http.client.KoogHttpClientException: Error from client: OllamaClient
Message: Exception during streaming: Flow invariant is violated:
Flow was collected in [com.mvlog.coroutines.exception.DefaultCoroutineExceptionHandler@a70aeb8, ProducerCoroutine{Active}@13db31d8, DarwinGlobalQueueDispatcher@4fa8208],
but emission happened in [com.mvlog.coroutines.exception.DefaultCoroutineExceptionHandler@a70aeb8, DispatchedCoroutine{Active}@abd12c0, Dispatchers.IO].
Please refer to 'flow' documentation or use 'flowOn' instead
at 0 Thoon.debug.dylib 0x10c3f1543 kfun:kotlin.Exception#<init>(kotlin.String?;kotlin.Throwable?){} + 123
at 1 Thoon.debug.dylib 0x10b216dcf kfun:ai.koog.http.client.KoogHttpClientException#<init>(kotlin.String?;kotlin.Int?;kotlin.String?;kotlin.String?;kotlin.Throwable?){} + 2175
at 2 Thoon.debug.dylib 0x10b2170df kfun:ai.koog.http.client.KoogHttpClientException#<init>(kotlin.String?;kotlin.Int?;kotlin.String?;kotlin.String?;kotlin.Throwable?;kotlin.Int;kotlin.native.internal.DefaultConstructorMarker?){} + 675
at 3 Thoon.debug.dylib 0x10cb1aeef kfun:ai.koog.http.client.ktor.KtorKoogHttpClient.KtorKoogHttpClient$lines$1.$invokeCOROUTINE$5.invokeSuspend#internal + 3775
at 4 Thoon.debug.dylib 0x10c3f0ac7 kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#invokeSuspend(kotlin.Result<kotlin.Any?>){}kotlin.Any? + 67
at 5 Thoon.debug.dylib 0x10c3ef767 kfun:kotlin.coroutines.native.internal.BaseContinuationImpl#resumeWith(kotlin.Result<kotlin.Any?>){} + 675
at 6 Thoon.debug.dylib 0x10c3f115b kfun:kotlin.coroutines.Continuation#resumeWith(kotlin.Result<1:0>){} + 91
at 7 Thoon.debug.dylib 0x10c5d104b kfun:kotlinx.coroutines.DispatchedTask#run(){} + 2035
at 8 Thoon.debug.dylib 0x10c600f3b kfun:kotlinx.coroutines.Runnable#run(){} + 83
at 9 Thoon.debug.dylib 0x10c5fc33b kfun:kotlinx.coroutines.DarwinGlobalQueueDispatcher.DarwinGlobalQueueDispatcher$dispatch$$inlined$autoreleasepool$1.invoke#internal + 71
at 10 Thoon.debug.dylib 0x10c5fc393 kfun:kotlinx.coroutines.DarwinGlobalQueueDispatcher.DarwinGlobalQueueDispatcher$dispatch$$inlined$autoreleasepool$1.$<bridge-DN>invoke(){}#internal + 71
at 11 Thoon.debug.dylib 0x10c3d5c7b kfun:kotlin.Function0#invoke(){}1:0 + 91
at 12 Thoon.debug.dylib 0x10c5fc22b _6f72672e6a6574627261696e732e6b6f746c696e783a6b6f746c696e782d636f726f7574696e65732d636f72652f6f70742f6275696c644167656e742f776f726b2f343465633665383530643563363366302f6b6f746c696e782d636f726f7574696e65732d636f72652f6e617469766544617277696e2f7372632f44697370617463686572732e6b74_4_knbridge3 + 239
at 13 libdispatch.dylib 0x1801c07a7 _dispatch_call_block_and_release + 23
at 14 libdispatch.dylib 0x1801db4af _dispatch_client_callout + 11
at 15 libdispatch.dylib 0x1801f6823 _dispatch_workloop_invoke.cold.4 + 27
at 16 libdispatch.dylib 0x1801d3d57 _dispatch_root_queue_drain + 915
at 17 libdispatch.dylib 0x1801d450f _dispatch_worker_thread2 + 251
at 18 libsystem_pthread.dylib 0x1043eab4b _pthread_wqthread + 227
at 19 libsystem_pthread.dylib 0x1043e998b start_wqthread + 7
Most likely the problem is here:
KtorKoogHttpClient invokes execute in lines function:
override fun <T : Any> lines(
path: String,
requestBody: T,
requestBodyType: KClass<T>,
parameters: Map<String, String>,
headers: Map<String, String>,
): Flow<String> = flow {
logger.debug { "Opening lines flow for $clientName" }
try {
ktorClient.preparePost(path) {
parameters.forEach { (key, value) ->
parameter(key, value)
}
applyRequestHeaders(headers)
if (requestBodyType == String::class) {
@Suppress("UNCHECKED_CAST")
setBody(requestBody as String)
} else {
setBody(requestBody, TypeInfo(requestBodyType))
}
}.execute { response: HttpResponse ->
if (!response.status.isSuccess()) {
throw KoogHttpClientException(
clientName = clientName,
statusCode = response.status.value,
errorBody = response.bodyAsText(),
)
}
val channel = response.bodyAsChannel()
while (!channel.isClosedForRead) {
val line = channel.readUTF8Line() ?: break
if (line.isBlank()) continue
emit(line)
}
}
} catch (e: KoogHttpClientException) {
throw e
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
throw KoogHttpClientException(
clientName = clientName,
message = "Exception during streaming: ${e.message}",
cause = e,
)
}
}
Under the hood in latest version of ktor there's thread switching added for non-jvm targets in execute function in HttpStatement's execute (in ktor):
https://github.com/ktorio/ktor/blob/main/ktor-client/ktor-client-core/common/src/io/ktor/client/statement/HttpStatement.kt
There was a similar issue, most likely its just channelFlow that is needed here
#1775
Environment
Test device
IOS simulator IPhone 16e
IOS 26.2
Library versions
- Koog = 1.2.0
- Ktor = 3.6.0
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
raseln commentedon Sep 21, 2026
I dug into this and can reproduce it. The cause is on the Ktor side: since 3.4.0 (KTOR-9146)
HttpStatement.execute/bodyrun their block on the engine dispatcher on non-JVM targets, so theemitinsideKtorKoogHttpClient.lines(andsse, which goes throughHttpStatement.body) breaks theflow {}invariant. Same class of problem as #1775, one layer down. Koog still pins Ktor 3.3.3, which is why it only shows up when the app pulls in a newer Ktor — and it would hit Koog itself once #2202 lands.I have a fix (
channelFlowin bothlinesandsse, plus a regression test) that I verified on JVM and the iOS simulator against Ktor 3.5.2. I'll open a PR againstdevelopshortly, unless someone has already started on this.