fix(tradein-frontend): остановить retry-storm на 401 — refetchOnMount:false + backoff (#800)
Some checks failed
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / changes (push) Successful in 7s
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / build-browser (push) Blocked by required conditions
Deploy Trade-In / test (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Has been cancelled

На стойком 401/протухшей сессии фронт повторял /me + /trade-in/quota в
бесконечном цикле (184+ запросов): errored query + дефолтный
refetchOnMount=true → каждый новый observer (useMe дёргают RouteGuard,
Topbar, UserMenu, useBrand) ре-фетчил при mount = re-subscribe storm.
Глобально выключаем refetchOnMount/onReconnect в QueryClient + экспоненциальный
retryDelay (cap 30s); те же гарды на useQuota. retry-предикат 4xx→false уже был.
Graceful session-screen (RouteGuard/NoAccessScreen) уже на месте.

Closes #800
This commit is contained in:
Light1YT 2026-06-13 16:47:58 +05:00
parent e1442d3b1d
commit 6444dc0ebd
2 changed files with 21 additions and 2 deletions

View file

@ -11,13 +11,24 @@ export function Providers({ children }: { children: React.ReactNode }) {
queries: {
staleTime: 5 * 60_000,
refetchOnWindowFocus: false,
// #770 finding #26: не ретраить 4xx (включая 429) — иначе на rate-limit
// дефолтные 3 ретрая превращаются в шторм. Ретраим только 5xx/сеть, ≤2 раза.
// #800: errored query + дефолтный refetchOnMount=true → каждый новый
// observer (Topbar/UserMenu/useBrand все дёргают useMe) ре-фетчит при
// mount → re-subscribe storm на стойком 401. Выключаем глобально: данные
// и так staleTime-кэшируются, а auth/quota не должны бить бэк на ремаунт.
refetchOnMount: false,
refetchOnReconnect: false,
// #770 finding #26 + #800: не ретраить 4xx (401/403/429) — иначе на
// rate-limit/протухшей сессии дефолтные 3 ретрая превращаются в шторм
// в собственный бэкенд. Ретраим только 5xx/сеть, ≤2 раза.
retry: (failureCount, error) => {
const status = (error as { status?: number } | null)?.status;
if (typeof status === "number" && status >= 400 && status < 500) return false;
return failureCount < 2;
},
// #800: экспоненциальный backoff на сетевых/5xx ретраях (1s, 2s, 4s…
// capped 30s) — никаких немедленных повторов, мягко к бэкенду.
retryDelay: (attemptIndex) =>
Math.min(1000 * 2 ** attemptIndex, 30_000),
},
mutations: {
retry: false,

View file

@ -10,6 +10,11 @@ const BASE = "/api/v1/trade-in";
* GET /api/v1/trade-in/quota
* Fetches the monthly evaluation quota status for the current session/account.
* Fail-open: if the query errors, the UI must NOT block the user (treat as unlimited).
*
* #800: `retry:false` на 401/протухшей сессии quota НЕ должна ре-стучаться в бэк.
* `refetchOnMount/Reconnect/WindowFocus:false` иначе errored quota ре-фетчит на
* каждый ремаунт владельца вклад в retry-storm. Освежается явным
* `invalidateQueries(["trade-in","quota"])` после успешной оценки (см. page.tsx).
*/
export function useQuota() {
return useQuery<QuotaStatus>({
@ -17,5 +22,8 @@ export function useQuota() {
queryFn: () => apiFetch<QuotaStatus>(`${BASE}/quota`),
staleTime: 30_000,
retry: false,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
});
}