From 3aa2a6f84386abfb6bacbc1fc0ad87518dac547c Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Mon, 26 Oct 2026 15:27:29 +0100 Subject: [PATCH 1/2] fix: Avoid infinite loop on unexpected errors in notifications worker scheduling Before, a 401 error could lead to a rescheduling loop. --- .../AbstractNotificationsRegistrationWorker.kt | 17 ++++++++++++----- .../kotlin/NotificationsRegistrationManager.kt | 6 +++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Notifications/Registration/src/main/kotlin/AbstractNotificationsRegistrationWorker.kt b/Notifications/Registration/src/main/kotlin/AbstractNotificationsRegistrationWorker.kt index 234ffd5c8..5a7964bb2 100644 --- a/Notifications/Registration/src/main/kotlin/AbstractNotificationsRegistrationWorker.kt +++ b/Notifications/Registration/src/main/kotlin/AbstractNotificationsRegistrationWorker.kt @@ -23,6 +23,7 @@ import androidx.work.CoroutineWorker import androidx.work.ExistingWorkPolicy import androidx.work.NetworkType import androidx.work.OneTimeWorkRequest +import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkerParameters import androidx.work.await @@ -40,6 +41,7 @@ import io.ktor.http.isSuccess import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.tasks.await import okhttp3.OkHttpClient @@ -75,6 +77,9 @@ abstract class AbstractNotificationsRegistrationWorker( }.awaitAll() return if (outcomes.any { it == Outcome.ShouldRetry }) { Result.retry() + } else if (outcomes.any { it == Outcome.Failed }) { + SentryLog.i(TAG, "Work failed") + Result.failure() } else { SentryLog.i(TAG, "Work finished") Result.success() @@ -128,9 +133,10 @@ abstract class AbstractNotificationsRegistrationWorker( return workRequest.id } - suspend fun awaitWorkerCompletion(uuid: UUID) { + suspend fun awaitWorkerCompletion(uuid: UUID): Boolean { val workManager = WorkManager.getInstance(appCtx) - workManager.getWorkInfoByIdFlow(uuid).first { it?.state?.isFinished == true } + val workInfo = workManager.getWorkInfoByIdFlow(uuid).filterNotNull().first { it.state.isFinished } + return workInfo.state != WorkInfo.State.FAILED } @PublishedApi @@ -140,6 +146,7 @@ abstract class AbstractNotificationsRegistrationWorker( private enum class Outcome { Done, + Failed, ShouldRetry, } @@ -155,11 +162,11 @@ private fun handleUnsuccessfulResponse(response: HttpResponse): Outcome { } 401 -> { SentryLog.w(TAG, errorMessage) - Outcome.Done + Outcome.Failed } else -> { SentryLog.wtf(TAG, errorMessage) - Outcome.Done + Outcome.Failed } } } @@ -171,6 +178,6 @@ private fun handleNetworkOperationFailure(t: Throwable): Outcome { Outcome.ShouldRetry } else { SentryLog.wtf(TAG, errorMessage, t) - Outcome.Done + Outcome.Failed } } diff --git a/Notifications/Registration/src/main/kotlin/NotificationsRegistrationManager.kt b/Notifications/Registration/src/main/kotlin/NotificationsRegistrationManager.kt index 092fe4a2c..cf461bbd1 100644 --- a/Notifications/Registration/src/main/kotlin/NotificationsRegistrationManager.kt +++ b/Notifications/Registration/src/main/kotlin/NotificationsRegistrationManager.kt @@ -118,11 +118,11 @@ object NotificationsRegistrationManager : AssociatedUserDataCleanable { latestFcmToken.collectLatest { fcmToken -> userIdsFlow.collectLatest { userIds -> - while (true) { + do { awaitAnyNeedForUpdate(userIds, fcmToken, latestNotificationTopics) val id = AbstractNotificationsRegistrationWorker.schedule(workerClass) - AbstractNotificationsRegistrationWorker.awaitWorkerCompletion(id) // Don't loop immediately. - } + val didNotFail = AbstractNotificationsRegistrationWorker.awaitWorkerCompletion(id) // Don't loop immediately. + } while (didNotFail) } } } From 59443c1c3d1b44301c93acc5acdf030ca6aa7c95 Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Mon, 26 Oct 2026 15:28:33 +0100 Subject: [PATCH 2/2] chore: Don't check for content-type when the http status is not successful This will help the status code getting through when a backend proxy replies with HTML. --- Network/Ktor/src/main/kotlin/CreateHttpClient.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Network/Ktor/src/main/kotlin/CreateHttpClient.kt b/Network/Ktor/src/main/kotlin/CreateHttpClient.kt index d964c8567..f14598047 100644 --- a/Network/Ktor/src/main/kotlin/CreateHttpClient.kt +++ b/Network/Ktor/src/main/kotlin/CreateHttpClient.kt @@ -30,6 +30,7 @@ import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.request import io.ktor.http.ContentType import io.ktor.http.HttpHeaders +import io.ktor.http.isSuccess import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json @@ -56,7 +57,7 @@ fun createHttpClient( } HttpResponseValidator { validateResponse { response -> - response.validateContentType { accepted, received -> + if (response.status.isSuccess()) response.validateContentType { accepted, received -> val url = response.request.url val method = response.request.method throw IllegalArgumentException("Expected Content-Type $accepted but got $received from $method on $url")