From b79ae8e7f1dbe9b91366b007bf056774bada9370 Mon Sep 17 00:00:00 2001 From: Moe Hamade <69801237+moehamade@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:13:45 +0300 Subject: [PATCH] fix: stop reporting a Tor wait during the fetch, and serve the cache on the rate limit that triggers the gate Addresses review on #812. The awaiting-route phase was never cleared. Reporting it before the wait fixed the label on the wait itself, but nothing restored ResolvingRelease afterwards, so the UI and notification claimed "Waiting for Tor" for the whole metadata request and its retries -- and in direct mode, where the wait returns immediately, for a wait that never happened. An onResolvingRelease callback now fires once the route is ready. A rate-limit rejection recorded the gate and returned the failure, while the stale-cache fallback sat at the top of the function and was only reached on a later call. The first About check therefore reported an error the user could not act on, and an immediate retry succeeded from metadata that was already present. The response that sets the gate now serves the cached release straight away. Co-Authored-By: Claude Opus 5 (1M context) --- .../android/util/GitHubReleaseClient.kt | 18 ++++++++++++++++++ .../android/util/UniversalApkManager.kt | 3 +++ 2 files changed, 21 insertions(+) diff --git a/app/src/main/java/com/bitchat/android/util/GitHubReleaseClient.kt b/app/src/main/java/com/bitchat/android/util/GitHubReleaseClient.kt index d33c1531..25922b27 100644 --- a/app/src/main/java/com/bitchat/android/util/GitHubReleaseClient.kt +++ b/app/src/main/java/com/bitchat/android/util/GitHubReleaseClient.kt @@ -69,6 +69,7 @@ object GitHubReleaseClient { suspend fun fetchLatestRelease( forceRefresh: Boolean = false, onAwaitingNetworkRoute: (() -> Unit)? = null, + onResolvingRelease: (() -> Unit)? = null, ): Result = withContext(Dispatchers.IO) { fetchMutex.withLock { @@ -108,6 +109,11 @@ object GitHubReleaseClient { ) ) } + // The wait is over, so stop saying we are waiting. In direct mode it + // returned immediately and never really started, and the fetch below + // retries -- either way the caller must not keep reporting a Tor wait + // for the whole metadata request. + onResolvingRelease?.invoke() var lastFailure: Throwable = ReleaseFetchException( "Failed to fetch the latest release from GitHub" @@ -121,6 +127,18 @@ object GitHubReleaseClient { } lastFailure = result.exceptionOrNull() ?: lastFailure + // The response that just set the gate is the one the user is waiting + // on. Reporting an error here and only serving the cache on the next + // call makes the first check fail and an immediate retry succeed from + // metadata we already had. + if (System.currentTimeMillis() < blockedUntilMillis) { + cached?.let { + Log.w(TAG, "Rate limited; serving the cached release instead of failing") + return@withLock Result.success(it.release) + } + return@withLock Result.failure(lastFailure) + } + if (!isRetryable(lastFailure) || attempt == MAX_FETCH_ATTEMPTS - 1) { return@withLock Result.failure(lastFailure) } diff --git a/app/src/main/java/com/bitchat/android/util/UniversalApkManager.kt b/app/src/main/java/com/bitchat/android/util/UniversalApkManager.kt index 47789582..a457c8b2 100644 --- a/app/src/main/java/com/bitchat/android/util/UniversalApkManager.kt +++ b/app/src/main/java/com/bitchat/android/util/UniversalApkManager.kt @@ -228,6 +228,9 @@ class UniversalApkManager(private val context: Context) { val release = GitHubReleaseClient.fetchLatestRelease( onAwaitingNetworkRoute = { phaseCallback?.invoke(ApkDownloader.DownloadPhase.AwaitingNetworkRoute) + }, + onResolvingRelease = { + phaseCallback?.invoke(ApkDownloader.DownloadPhase.ResolvingRelease) } ).getOrElse { error -> return@withContext Result.failure(error)