Fix Firebase image-loading crash: use OkHttp Ktor engine explicitly - #3052
Merged
Merged
Conversation
Fixes the Crashlytics crash "Exception in completion handler ... Cancelling" whose root cause is "java.lang.IllegalStateException: Unbalanced enter/exit" thrown from the platform's bundled com.android.okhttp AsyncTimeout. Chain of events: Coil forgets an AsyncImagePainter while an image request is in flight (composable leaving composition), cancels the Ktor load job, which closes the underlying HTTP stream mid-read. That stream runs on the platform Android engine (java.net.HttpURLConnection -> com.android.okhttp), a frozen ~2015 okhttp fork whose AsyncTimeout throws "Unbalanced enter/exit" on a concurrent close. The exception is raised inside a coroutine cancellation handler on the main thread during Compose applyChanges, so nothing catches it and the app dies. The platform engine was never chosen deliberately. The Jan 2026 OkHttp->Ktor migration (b93c73e) dropped authlib's explicit pre-configured OkHttp engine in favor of a bare HttpClient { }, which selects an engine via ServiceLoader from whatever is on the classpath. The only engine present was ktor-client-android (pulled in by auth-core-public), so every request, including Coil image loads sharing the base client, silently ran on the platform fork. Fix: select the engine explicitly per platform instead of relying on implicit ServiceLoader resolution. - network-clients (base client behind Coil, Apollo and all app traffic): androidMain and jvmMain use OkHttp (maintained okhttp3 5.4.0, already shipping for Datadog), nativeMain uses Darwin. iOS behavior is unchanged. - authlib: same expect/actual engine selection (jvm OkHttp, ios Darwin); removed the dangling HttpClientEngine import left by the migration. - auth-core-public: dropped its ktor-client-okhttp dependency; it builds no client and only fed authlib's implicit engine off the classpath. - Version catalog: added ktor-client-okhttp, removed unused ktor-client-android and ktor-client-cio. A missing or wrong engine is now a compile-time error per platform rather than a silent runtime default, so this class of regression cannot recur unnoticed. Verified: :app:assembleDebug plus iosSimulatorArm64 compilation of both modules and ktlint all pass.
panasetskaya
approved these changes
Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The firebase crash as I understand originates from the fact that we were not explicitly using the latest Okhttp engine for ktor to use, so it was defaulting to a built-in one in Android which is not the latest and greatest.
This adds the engine explicitly which should make it use the latest properly.
AI description below:
What & why
Fixes the Crashlytics/Firebase crash "Exception in completion handler …
Cancelling", whose true root cause (bottom of the causal chain) is:Seen on production 14.4.2 (e.g. Pixel 7 / Android 17, Xiaomi 24115RA8EG / Android 16), both while the Chat screen was loading images.
Chain of events
AsyncImagePainterwhile an image request is in flight (composable leaving composition) and cancels the Ktor load job.java.net.HttpURLConnection→com.android.okhttp), a frozen ~2015 okhttp fork whoseAsyncTimeoutthrowsUnbalanced enter/exiton a concurrent close.applyChanges, so nothing catches it and the app dies.How we ended up on the platform engine
The platform engine was never chosen deliberately. The Jan 2026 OkHttp→Ktor migration (
b93c73e3) dropped authlib's explicit pre-configured OkHttp engine in favour of a bareHttpClient { }, which resolves an engine via ServiceLoader from whatever is on the classpath. The only engine present wasktor-client-android(pulled in byauth-core-public), so every request, including Coil image loads sharing the base client, silently ran on the platform fork. Maintainedcom.squareup.okhttp3handles the concurrent cancel cleanly and never throws on this path.The fix
Select the engine explicitly per platform instead of relying on implicit ServiceLoader resolution.
network-clients(base client: Coil + Apollo + all app traffic)authlib(auth-token client)commonMaingainsexpect fun httpClientEngineFactory(), used inbuildKtorClient; actuals inandroidMain/jvmMain(OkHttp, the maintained okhttp3 5.4.0 already shipping for Datadog) andnativeMain(Darwin).HttpClientEngineimport the migration left behind.ktor-client-okhttpdependency. It builds no client and only fed authlib's implicit engine off the classpath.ktor-client-okhttp, removed unusedktor-client-androidandktor-client-cio.iOS behaviour is unchanged (still Darwin/NSURLSession); the crash was Android-only. A missing or wrong engine is now a compile-time error per platform rather than a silent runtime default, so this class of regression cannot recur unnoticed.
Verification
:app:assembleDebug✅:network-clients:compileKotlinIosSimulatorArm64+:authlib:compileKotlinIosSimulatorArm64✅ktlinton both changed modules ✅