From 5ee77a7eb3e07c6c9222c85286e58170593a3097 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sat, 6 Jun 2026 13:41:13 +0000 Subject: [PATCH] fix(mobile): retry MFA/auth requests on transport/IO aborts (stale-connection) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports the stranded fix from fix/mfa-request-retry-stale-connection (was 24 commits behind main, never merged or released) onto current main, resolving the NetworkModule.kt conflict. The identity HttpClient now installs Ktor HttpRequestRetry (maxRetries=2, exponentialDelay) that retries ONLY on transport/IO exceptions (IOException / SocketTimeout / ConnectTimeout / ClosedReceiveChannelException) — never on 4xx/5xx, so a consumed MFA code is never resubmitted; the body is a serialized object, fully replayable. Fixes the OkHttp h2 stale-connection abort that the server logged as "Malformed request body: I/O error while reading input message" (misdiagnosed as slow-uplink truncation). Verified: :shared:compileDebugKotlinAndroid passes (JDK 21, Ktor 3.1.1). Reaches devices on the next APK build. Surfaced by scripts/drift-check.sh. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../com/fivucsas/shared/di/NetworkModule.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/shared/src/commonMain/kotlin/com/fivucsas/shared/di/NetworkModule.kt b/shared/src/commonMain/kotlin/com/fivucsas/shared/di/NetworkModule.kt index 1b67caee..b663bf25 100644 --- a/shared/src/commonMain/kotlin/com/fivucsas/shared/di/NetworkModule.kt +++ b/shared/src/commonMain/kotlin/com/fivucsas/shared/di/NetworkModule.kt @@ -37,6 +37,7 @@ import com.fivucsas.shared.data.remote.dto.OAuthTokenResponseDto import com.fivucsas.shared.data.remote.dto.toModel import io.ktor.client.HttpClient import io.ktor.client.call.body +import io.ktor.client.plugins.HttpRequestRetry import io.ktor.client.plugins.HttpSend import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.plugin @@ -224,6 +225,25 @@ val networkModule = module { socketTimeoutMillis = ApiConfig.SOCKET_TIMEOUT_MS } + // Resilience against stale-connection aborts (e.g. OkHttp reusing a + // half-closed HTTP/2 keep-alive connection that Traefik dropped while + // the app was idle → the POST body is RST mid-send → the server logs + // "Malformed request body: I/O error while reading input message"). + // This is bandwidth-independent and was misdiagnosed as a slow-uplink + // truncation. Retry ONLY on transport/IO exceptions (NOT on 4xx/5xx), + // so a consumed MFA code is never resubmitted; the body is a serialized + // object, so it is fully replayable on retry. + install(HttpRequestRetry) { + retryOnExceptionIf(maxRetries = 2) { _, cause -> + val name = cause::class.simpleName ?: "" + name.contains("IOException") || + name.contains("SocketTimeout") || + name.contains("ConnectTimeout") || + name == "ClosedReceiveChannelException" + } + exponentialDelay() + } + defaultRequest { url(ApiConfig.identityBaseUrl + "/")