From f89eb954a84a8d667e9987f08aba7f3eb16e2448 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Wed, 6 May 2026 10:44:30 +0500 Subject: [PATCH 1/3] fix(search): convert backend RateLimitedException to RateLimitException so direct-GitHub fallback storm doesn't fire --- .../data/repository/SearchRepositoryImpl.kt | 39 +++++++++++++------ .../search/presentation/SearchViewModel.kt | 11 +++++- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt index a26d3d85..f2915d73 100644 --- a/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt +++ b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt @@ -26,6 +26,7 @@ import zed.rainxch.core.data.mappers.toSummary import zed.rainxch.core.data.network.BackendApiClient import zed.rainxch.core.data.network.GitHubClientProvider import zed.rainxch.core.data.network.executeRequest +import zed.rainxch.core.data.network.shouldFallbackToGithubOrRethrow import zed.rainxch.core.domain.model.DiscoveryPlatform import zed.rainxch.core.domain.model.GithubRepoSummary import zed.rainxch.core.domain.model.PaginatedDiscoveryRepositories @@ -142,18 +143,32 @@ class SearchRepositoryImpl( offset = offset, ) - return result.getOrNull()?.let { searchResponse -> - val repos = searchResponse.items.map { it.toSummary() } - - val hasMore = offset + repos.size < searchResponse.totalHits - PaginatedDiscoveryRepositories( - repos = repos, - hasMore = hasMore, - nextPageIndex = page + 1, - totalCount = searchResponse.totalHits, - passthroughAttempted = searchResponse.passthroughAttempted, - ) - } + return result.fold( + onSuccess = { searchResponse -> + val repos = searchResponse.items.map { it.toSummary() } + val hasMore = offset + repos.size < searchResponse.totalHits + PaginatedDiscoveryRepositories( + repos = repos, + hasMore = hasMore, + nextPageIndex = page + 1, + totalCount = searchResponse.totalHits, + passthroughAttempted = searchResponse.passthroughAttempted, + ) + }, + onFailure = { e -> + // Centralized fallback policy: throws RateLimitException + // for backend 429 (caller surfaces friendly retry-after + // toast) so we don't cascade into a direct-GitHub + // /search/repositories + per-repo /releases verify storm + // that would burn the user's GitHub quota and trip the + // global rate-limit dialog. 5xx / network errors fall + // through to the GitHub REST fallback as before. + if (!shouldFallbackToGithubOrRethrow(e)) { + return null + } + null + }, + ) } // ── Fallback GitHub REST search ─────────────────────────────────── diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt index b3466b13..9fffc6a5 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt @@ -41,6 +41,9 @@ import zed.rainxch.githubstore.core.presentation.res.failed_to_share_link import zed.rainxch.githubstore.core.presentation.res.link_copied_to_clipboard import zed.rainxch.githubstore.core.presentation.res.no_github_link_in_clipboard import zed.rainxch.githubstore.core.presentation.res.explore_error +import zed.rainxch.githubstore.core.presentation.res.rate_limit_exceeded +import zed.rainxch.githubstore.core.presentation.res.rate_limit_exceeded_retry_in +import zed.rainxch.githubstore.core.presentation.res.rate_limit_exceeded_signin_hint import zed.rainxch.githubstore.core.presentation.res.search_failed import zed.rainxch.search.presentation.mappers.toDomain import zed.rainxch.search.presentation.utils.isEntirelyGithubUrls @@ -404,11 +407,17 @@ class SearchViewModel( } } catch (e: RateLimitException) { logger.debug("Rate limit exceeded: ${e.message}") + val seconds = e.rateLimitInfo.timeUntilReset().inWholeSeconds + val message = if (seconds > 0L) { + getString(Res.string.rate_limit_exceeded_retry_in, seconds.toInt()) + } else { + getString(Res.string.rate_limit_exceeded) + } _state.update { it.copy( isLoading = false, isLoadingMore = false, - errorMessage = e.message, + errorMessage = message, ) } } catch (e: CancellationException) { From 15f441508527566e11bc60dd656b687c5ba4a357 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Wed, 6 May 2026 10:44:30 +0500 Subject: [PATCH 2/3] chore(strings): note search rate-limit cascade fix in 1.8.1 what's-new across locales --- .../src/commonMain/composeResources/files/whatsnew/16.json | 3 ++- .../src/commonMain/composeResources/files/whatsnew/ar/16.json | 3 ++- .../src/commonMain/composeResources/files/whatsnew/bn/16.json | 3 ++- .../src/commonMain/composeResources/files/whatsnew/es/16.json | 3 ++- .../src/commonMain/composeResources/files/whatsnew/fr/16.json | 3 ++- .../src/commonMain/composeResources/files/whatsnew/hi/16.json | 3 ++- .../src/commonMain/composeResources/files/whatsnew/it/16.json | 3 ++- .../src/commonMain/composeResources/files/whatsnew/ja/16.json | 3 ++- .../src/commonMain/composeResources/files/whatsnew/ko/16.json | 3 ++- .../src/commonMain/composeResources/files/whatsnew/pl/16.json | 3 ++- .../src/commonMain/composeResources/files/whatsnew/ru/16.json | 3 ++- .../src/commonMain/composeResources/files/whatsnew/tr/16.json | 3 ++- .../commonMain/composeResources/files/whatsnew/zh-CN/16.json | 3 ++- 13 files changed, 26 insertions(+), 13 deletions(-) diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/16.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/16.json index b0be4086..2022c5e8 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/16.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/16.json @@ -38,7 +38,8 @@ "Windows installer launch no longer fails on accounts with non-ASCII usernames or unusual filename characters.", "Liquid Glass effect removed — icons in dark mode could become invisible against transparent backgrounds. Cleaner, higher-contrast UI everywhere.", "Store no longer claims an update is available for itself after you've already updated it.", - "Apps screen — Add-by-link button no longer covers the last app in the list." + "Apps screen — Add-by-link button no longer covers the last app in the list.", + "Search no longer triggers the GitHub rate-limit dialog when the backend itself is busy — surfaces a friendly retry-after toast instead." ] } ] diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/ar/16.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/ar/16.json index 663d6ade..f433652a 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/ar/16.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/ar/16.json @@ -38,7 +38,8 @@ "تشغيل المثبّت على Windows لم يعد يفشل في الحسابات ذات أسماء المستخدمين غير ASCII أو الملفات بأسماء غير اعتيادية.", "تمت إزالة تأثير الزجاج السائل — قد تختفي الأيقونات في الوضع الداكن على الخلفيات الشفافة. واجهة أنظف بتباين أعلى في كل مكان.", "المتجر لم يعد يدّعي توفر تحديث لنفسه بعد أن تكون قد حدّثته بالفعل.", - "شاشة التطبيقات — زر «إضافة عبر رابط» لم يعد يغطي آخر تطبيق في القائمة." + "شاشة التطبيقات — زر «إضافة عبر رابط» لم يعد يغطي آخر تطبيق في القائمة.", + "البحث لم يعد يُظهر نافذة تجاوز حد الطلبات في GitHub عندما يكون الخادم الخلفي مشغولاً — يعرض رسالة «حاول لاحقاً» مختصرة بدلاً من ذلك." ] } ] diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/bn/16.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/bn/16.json index fdc2bd4c..a3fc6572 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/bn/16.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/bn/16.json @@ -38,7 +38,8 @@ "Windows ইনস্টলার চালু করা আর non-ASCII ইউজারনেম বা অস্বাভাবিক ফাইলনেমে ব্যর্থ হয় না।", "লিকুইড গ্লাস ইফেক্ট সরানো হয়েছে — ডার্ক মোডে স্বচ্ছ ব্যাকগ্রাউন্ডে আইকন অদৃশ্য হয়ে যেতে পারত। সর্বত্র পরিষ্কার, উচ্চ-কনট্রাস্ট UI।", "স্টোর আপনি ইতিমধ্যে আপডেট করার পরে আর নিজের জন্য আপডেট আছে দাবি করে না।", - "Apps স্ক্রিন — «লিঙ্ক দিয়ে যোগ করুন» বোতাম আর তালিকার শেষ অ্যাপটি ঢেকে রাখে না।" + "Apps স্ক্রিন — «লিঙ্ক দিয়ে যোগ করুন» বোতাম আর তালিকার শেষ অ্যাপটি ঢেকে রাখে না।", + "ব্যাকএন্ড ব্যস্ত থাকলে সার্চ আর GitHub রেট-লিমিট ডায়ালগ দেখায় না — এর বদলে একটি 'পরে চেষ্টা করুন' টোস্ট দেখায়।" ] } ] diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/es/16.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/es/16.json index 6b39ddd2..b6b34574 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/es/16.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/es/16.json @@ -38,7 +38,8 @@ "El lanzamiento del instalador en Windows ya no falla con cuentas que tienen nombres de usuario no ASCII o nombres de archivo inusuales.", "Efecto de cristal líquido eliminado — los iconos en modo oscuro podían volverse invisibles sobre fondos transparentes. Interfaz más limpia y con mayor contraste en todas partes.", "La tienda ya no anuncia una actualización propia disponible después de que la hayas actualizado.", - "Pantalla de Apps — el botón «Añadir por enlace» ya no tapa la última app de la lista." + "Pantalla de Apps — el botón «Añadir por enlace» ya no tapa la última app de la lista.", + "La búsqueda ya no dispara el diálogo de límite de peticiones de GitHub cuando el backend está ocupado — muestra un toast amigable de «inténtalo en breve»." ] } ] diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/fr/16.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/fr/16.json index d6b22477..fa991301 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/fr/16.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/fr/16.json @@ -38,7 +38,8 @@ "Le lancement de l’installateur sous Windows ne plante plus pour les comptes dont le nom d’utilisateur contient des caractères non-ASCII ou des noms de fichier inhabituels.", "Effet verre liquide supprimé — les icônes en mode sombre pouvaient devenir invisibles sur des fonds transparents. Interface plus nette et plus contrastée partout.", "Le store ne signale plus une mise à jour disponible pour lui-même après que vous l'avez déjà mis à jour.", - "Écran Apps — le bouton « Ajouter par lien » ne masque plus la dernière app de la liste." + "Écran Apps — le bouton « Ajouter par lien » ne masque plus la dernière app de la liste.", + "La recherche ne déclenche plus la boîte de dialogue de limitation de débit GitHub quand le backend est occupé — affiche un toast « réessayez bientôt » à la place." ] } ] diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/hi/16.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/hi/16.json index c64e5a76..08eeb7d1 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/hi/16.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/hi/16.json @@ -38,7 +38,8 @@ "Windows इंस्टॉलर लॉन्च अब non-ASCII यूज़रनेम या असामान्य फ़ाइलनेम वाले अकाउंट पर विफल नहीं होता।", "लिक्विड ग्लास इफ़ेक्ट हटा दिया गया — डार्क मोड में पारदर्शी पृष्ठभूमि पर आइकन अदृश्य हो सकते थे। हर जगह साफ़, उच्च-कंट्रास्ट UI।", "स्टोर अब पहले से अपडेट होने के बाद अपने लिए अपडेट उपलब्ध होने का दावा नहीं करता।", - "Apps स्क्रीन — 'लिंक से जोड़ें' बटन अब सूची के आख़िरी ऐप को नहीं ढकता।" + "Apps स्क्रीन — 'लिंक से जोड़ें' बटन अब सूची के आख़िरी ऐप को नहीं ढकता।", + "बैकएंड व्यस्त होने पर सर्च अब GitHub रेट-लिमिट डायलॉग नहीं दिखाती — इसके बजाय 'बाद में प्रयास करें' टोस्ट दिखाती है।" ] } ] diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/it/16.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/it/16.json index 01b3be2d..07d83485 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/it/16.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/it/16.json @@ -38,7 +38,8 @@ "L’avvio dell’installer su Windows non fallisce più per account con nomi utente non ASCII o nomi file insoliti.", "Effetto vetro liquido rimosso — le icone in modalità scura potevano diventare invisibili su sfondi trasparenti. Interfaccia più pulita e con maggior contrasto ovunque.", "Lo store non segnala più un aggiornamento disponibile per sé stesso dopo che lo hai già aggiornato.", - "Schermata App — il pulsante «Aggiungi tramite link» non copre più l'ultima app dell'elenco." + "Schermata App — il pulsante «Aggiungi tramite link» non copre più l'ultima app dell'elenco.", + "La ricerca non mostra più il dialogo di limite richieste di GitHub quando è il backend a essere occupato — mostra invece un toast «riprova a breve»." ] } ] diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/ja/16.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/ja/16.json index 4592f1f9..1c7d6015 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/ja/16.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/ja/16.json @@ -38,7 +38,8 @@ "Windows のインストーラー起動が、非 ASCII のユーザー名や特殊な文字を含むファイル名でも失敗しなくなりました。", "リキッドグラス効果を削除しました — ダークモードでは透明な背景にアイコンが見えなくなることがありました。全体的にすっきりした、コントラストの高い UI になりました。", "ストアを更新したあとも自分自身に更新があると表示し続ける問題を修正しました。", - "アプリ画面 — 「リンクで追加」ボタンが一覧の最後のアプリを覆わなくなりました。" + "アプリ画面 — 「リンクで追加」ボタンが一覧の最後のアプリを覆わなくなりました。", + "バックエンドが混雑しているときに検索が GitHub のレート制限ダイアログを出さなくなりました — 代わりに「しばらくしてから再試行」のトーストを表示します。" ] } ] diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.json index 7ad99ee2..fc99df78 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.json @@ -38,7 +38,8 @@ "Windows 설치 프로그램 실행이 비 ASCII 사용자 이름 또는 특수 문자가 포함된 파일 이름에서도 실패하지 않습니다.", "리퀴드 글라스 효과 제거 — 다크 모드의 투명 배경 위에서 아이콘이 보이지 않을 수 있었습니다. 모든 곳에서 더 깔끔하고 대비가 높은 UI로 개선되었습니다.", "스토어를 업데이트한 뒤에도 자신에 대해 업데이트가 있다고 계속 알리던 문제를 수정했습니다.", - "앱 화면 — '링크로 추가' 버튼이 목록의 마지막 앱을 가리지 않습니다." + "앱 화면 — '링크로 추가' 버튼이 목록의 마지막 앱을 가리지 않습니다.", + "백엔드가 바쁠 때 검색이 더 이상 GitHub 속도 제한 대화상자를 띄우지 않습니다 — 대신 '잠시 후 다시 시도하세요' 토스트를 보여줍니다." ] } ] diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/pl/16.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/pl/16.json index 75f699c0..42c4e8b5 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/pl/16.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/pl/16.json @@ -38,7 +38,8 @@ "Uruchamianie instalatora w Windows nie zawodzi już na kontach z nazwami użytkownika zawierającymi znaki spoza ASCII lub nietypowymi nazwami plików.", "Efekt płynnego szkła usunięty — w trybie ciemnym ikony na przezroczystym tle mogły stawać się niewidoczne. Wszędzie czystszy, bardziej kontrastowy interfejs.", "Sklep nie zgłasza już dostępnej aktualizacji dla samego siebie po jej zainstalowaniu.", - "Ekran Aplikacji — przycisk „Dodaj przez link” nie zasłania już ostatniej aplikacji na liście." + "Ekran Aplikacji — przycisk „Dodaj przez link” nie zasłania już ostatniej aplikacji na liście.", + "Wyszukiwarka nie wyświetla już okna limitu zapytań GitHuba, gdy backend jest zajęty — zamiast tego pokazuje przyjazny toast „spróbuj ponownie za chwilę”." ] } ] diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/ru/16.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/ru/16.json index c2eb3ea7..fbe0b795 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/ru/16.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/ru/16.json @@ -38,7 +38,8 @@ "Запуск установщика на Windows больше не падает на учётных записях с не-ASCII именами пользователей или необычными именами файлов.", "Эффект жидкого стекла убран — в тёмной теме иконки могли становиться невидимыми на прозрачном фоне. Везде более чистый и контрастный интерфейс.", "Магазин больше не сообщает о доступном обновлении самого себя после того, как вы его уже обновили.", - "Экран приложений — кнопка «Добавить по ссылке» больше не закрывает последнее приложение в списке." + "Экран приложений — кнопка «Добавить по ссылке» больше не закрывает последнее приложение в списке.", + "Поиск больше не показывает диалог превышения лимита GitHub, когда занят сам бэкенд — вместо этого выводит дружелюбный тост «повторите позже»." ] } ] diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.json index 9ce45809..4a98db9b 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.json @@ -38,7 +38,8 @@ "Windows'ta yükleyici başlatma, ASCII dışı kullanıcı adları veya alışılmadık dosya adlarına sahip hesaplarda artık başarısız olmuyor.", "Sıvı cam efekti kaldırıldı — koyu modda ikonlar şeffaf arka planlarda görünmez olabiliyordu. Her yerde daha temiz, daha kontrastlı bir arayüz.", "Mağaza artık siz güncelledikten sonra kendisi için güncelleme mevcut olduğunu söylemiyor.", - "Uygulamalar ekranı — 'Bağlantıyla ekle' düğmesi artık listenin son uygulamasını örtmüyor." + "Uygulamalar ekranı — 'Bağlantıyla ekle' düğmesi artık listenin son uygulamasını örtmüyor.", + "Backend meşgulken arama artık GitHub hız sınırı diyaloğunu açmıyor — bunun yerine 'birazdan tekrar deneyin' bildirimi gösteriyor." ] } ] diff --git a/core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/16.json b/core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/16.json index 736810bb..70f108cb 100644 --- a/core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/16.json +++ b/core/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/16.json @@ -38,7 +38,8 @@ "修复 Windows 安装器在用户名包含非 ASCII 字符或文件名特殊的账户上无法启动的问题。", "移除液态玻璃效果 — 在深色模式下,透明背景上的图标可能会看不清。整体界面更干净、对比度更高。", "更新商店后,商店不会再继续提示自己有新版本可用。", - "应用页面 — 「通过链接添加」按钮不再遮挡列表中最后一个应用。" + "应用页面 — 「通过链接添加」按钮不再遮挡列表中最后一个应用。", + "后端繁忙时搜索不再弹出 GitHub 速率限制对话框 — 而是显示「请稍后重试」的提示。" ] } ] From acea29101c3c4ef9d95f0346fcd58c31d7c12e5b Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Wed, 6 May 2026 10:54:05 +0500 Subject: [PATCH 3/3] fix(search): rethrow non-fallback backend errors instead of silently retrying via GitHub direct --- .../data/repository/SearchRepositoryImpl.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt index f2915d73..7fe26c33 100644 --- a/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt +++ b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt @@ -156,15 +156,22 @@ class SearchRepositoryImpl( ) }, onFailure = { e -> - // Centralized fallback policy: throws RateLimitException - // for backend 429 (caller surfaces friendly retry-after - // toast) so we don't cascade into a direct-GitHub - // /search/repositories + per-repo /releases verify storm - // that would burn the user's GitHub quota and trip the - // global rate-limit dialog. 5xx / network errors fall - // through to the GitHub REST fallback as before. + // Centralized fallback policy. Side effects: + // * 429 → throws domain RateLimitException so the + // caller surfaces a friendly retry-after toast + // (prevents the direct-GitHub /search + per-repo + // /releases verify storm that would otherwise burn + // the user's quota and trip the global rate-limit + // dialog). + // * CancellationException → re-thrown to preserve + // structured concurrency. + // * BackendException 5xx / network → returns true → + // fall through to GitHub REST fallback below. + // * BackendException 4xx (other than 429) → returns + // false → backend's answer is authoritative; do + // NOT silently retry against direct GitHub. if (!shouldFallbackToGithubOrRethrow(e)) { - return null + throw e } null },