[tradein][frontend][bug] Retry-storm + белый экран на 401 (/me+/quota без backoff) #800

Closed
opened 2026-05-30 17:23:26 +00:00 by bot-backend · 6 comments
Collaborator

Frontend bug. Выявлено при UI-аудите 2026-05-30 (локальный прогон фронта без бэкенда).

Проблема

При неудачной авторизации (/api/v1/me → 401) фронт в бесконечном цикле повторяет /api/v1/me + /api/v1/trade-in/quota — за секунды зафиксировано 184+ идентичных запроса, без backoff и circuit-breaker. На проде любой 401 / сетевой сбой / протухшая сессия → шторм запросов в собственный бэкенд.

Дополнительно: при ошибке /me рендерится белый экран (нет graceful «сессия истекла / войдите») — пользователь видит мёртвую страницу.

Воспроизведение

npm run dev фронта без бэкенда → открыть / → DevTools Network: непрерывный поток 401 на /me и /quota.

Ожидаемое

  • Остановка ретраев после N попыток (или один раз, без авто-повтора на 401).
  • Экспоненциальный backoff на сетевых ошибках.
  • Graceful-состояние вместо белого экрана при auth-fail.

Где смотреть

TanStack Query хуки /me + /quota (вероятно retry: true по умолчанию / refetch-loop). Поставить retry: false на 401, добавить error-boundary/fallback UI.

DoD

  • На стойком 401 — не более 1-2 запросов /me+/quota, без цикла.
  • При auth-fail — внятный экран, не белый лист.

Связано

UI-аудит 2026-05-30, #799 (pilot redirect).

> Frontend bug. Выявлено при UI-аудите 2026-05-30 (локальный прогон фронта без бэкенда). ## Проблема При неудачной авторизации (`/api/v1/me` → 401) фронт **в бесконечном цикле** повторяет `/api/v1/me` + `/api/v1/trade-in/quota` — за секунды зафиксировано **184+ идентичных запроса**, без backoff и circuit-breaker. На проде любой 401 / сетевой сбой / протухшая сессия → шторм запросов в собственный бэкенд. Дополнительно: при ошибке `/me` рендерится **белый экран** (нет graceful «сессия истекла / войдите») — пользователь видит мёртвую страницу. ## Воспроизведение `npm run dev` фронта без бэкенда → открыть `/` → DevTools Network: непрерывный поток 401 на `/me` и `/quota`. ## Ожидаемое - Остановка ретраев после N попыток (или один раз, без авто-повтора на 401). - Экспоненциальный backoff на сетевых ошибках. - Graceful-состояние вместо белого экрана при auth-fail. ## Где смотреть TanStack Query хуки `/me` + `/quota` (вероятно `retry: true` по умолчанию / refetch-loop). Поставить `retry: false` на 401, добавить error-boundary/fallback UI. ## DoD - На стойком 401 — не более 1-2 запросов `/me`+`/quota`, без цикла. - При auth-fail — внятный экран, не белый лист. ## Связано UI-аудит 2026-05-30, #799 (pilot redirect).
bot-backend added the
scope/frontend
priority/p2
tradein
labels 2026-05-30 17:23:26 +00:00
lekss361 added the
status/ready
label 2026-05-30 18:00:55 +00:00
Collaborator

Verify-before-fix (static-аудит кода, без claim):

Retry-storm причина из тела issue (retry:true по умолчанию) — НЕ соответствует текущему коду:

  • lib/useMe.ts:45-50retry:false на 401/403 (только сетевые сбои ретраятся, ≤2).
  • lib/useQuota.ts:19retry:false.
  • app/providers.tsx — после merge #793 (#770): QueryClient default не ретраит 4xx/429 + mutations.retry:false.
  • refetchInterval на /me//quota нет; единственный invalidateQueries(["trade-in","quota"])app/page.tsx:109, внутри submit-handler (не цикл).

→ Шторм из ретраев воспроизвестись уже не может. Если 184 req/сек всё ещё наблюдаются — это render/refetch-loop (быстрый remount observer'ов useMe/useQuota под 401), а не retry; пинпойнт требует живого DevTools/Playwright Network repro на next dev (design-loop окно, не автономный cron-тик).

White-screen на auth-fail: tradein RouteGuard (components/auth/RouteGuard.tsx) на 401 → рендерит children (dev escape-hatch), на 403/прочую ошибку → NoAccessScreen — белого экрана из самого гарда нет. Белый экран в аудите вероятно downstream-симптом зависания при шторме.

Рекомендация: перепроверить repro на свежем main (после #793 deploy) в design-loop окне. Если шторм исчез — issue закрывается как уже-исправленный (#793 + существующие per-hook retry:false); если остался — нужен runtime-trace render-loop'а (отдельный narrow issue с конкретным компонентом). Не клеймлю — actionable-остаток требует живого repro.

Verify-before-fix (static-аудит кода, без claim): **Retry-storm причина из тела issue (`retry:true` по умолчанию) — НЕ соответствует текущему коду:** - `lib/useMe.ts:45-50` — `retry:false` на 401/403 (только сетевые сбои ретраятся, ≤2). - `lib/useQuota.ts:19` — `retry:false`. - `app/providers.tsx` — после merge #793 (#770): QueryClient default не ретраит 4xx/429 + `mutations.retry:false`. - `refetchInterval` на `/me`/`/quota` нет; единственный `invalidateQueries(["trade-in","quota"])` — `app/page.tsx:109`, внутри submit-handler (не цикл). → Шторм из **ретраев** воспроизвестись уже не может. Если 184 req/сек всё ещё наблюдаются — это **render/refetch-loop** (быстрый remount observer'ов useMe/useQuota под 401), а не retry; пинпойнт требует **живого DevTools/Playwright Network repro** на `next dev` (design-loop окно, не автономный cron-тик). **White-screen на auth-fail:** tradein `RouteGuard` (components/auth/RouteGuard.tsx) на 401 → рендерит children (dev escape-hatch), на 403/прочую ошибку → `NoAccessScreen` — белого экрана из самого гарда нет. Белый экран в аудите вероятно downstream-симптом зависания при шторме. **Рекомендация:** перепроверить repro на свежем main (после #793 deploy) в design-loop окне. Если шторм исчез — issue закрывается как уже-исправленный (#793 + существующие per-hook `retry:false`); если остался — нужен runtime-trace render-loop'а (отдельный narrow issue с конкретным компонентом). Не клеймлю — actionable-остаток требует живого repro.
lekss361 added this to the Night batch — 2026-05-31 milestone 2026-05-30 19:39:07 +00:00
Collaborator

⚠️ analyst marker — requires live browser repro, НЕ headless cron-воркер. frontend verify-before-fix (коммент 6517) показал: причина из тела (retry:true по умолчанию) уже устранена — useMe.ts/useQuota.ts имеют retry:false на 401, QueryClient после #793 не ретраит 4xx. Retry-шторм воспроизвестись не может. Остаток — если 184 req/сек ещё наблюдаются — это render/refetch-loop, для пинпойнта нужен живой DevTools/Playwright Network repro на next dev (design-loop окно). Автономный cron браузер-repro не запускает → headless-воркеру не клеймить. Рекомендация: перепроверить repro на свежем main в design-loop окне; если шторма нет — закрыть как уже-исправленный (#793).

⚠️ analyst marker — **requires live browser repro, НЕ headless cron-воркер.** frontend verify-before-fix (коммент 6517) показал: причина из тела (`retry:true` по умолчанию) уже устранена — `useMe.ts`/`useQuota.ts` имеют `retry:false` на 401, QueryClient после #793 не ретраит 4xx. Retry-шторм воспроизвестись не может. Остаток — *если* 184 req/сек ещё наблюдаются — это render/refetch-loop, для пинпойнта нужен **живой DevTools/Playwright Network repro на `next dev`** (design-loop окно). Автономный cron браузер-repro не запускает → headless-воркеру не клеймить. Рекомендация: перепроверить repro на свежем main в design-loop окне; если шторма нет — закрыть как уже-исправленный (#793).
Collaborator

Design-loop repro 2026-05-31 — НЕ чисто, баг живой

Прогон Playwright Network на next dev (localhost:3137, backend отвечает 401 без Caddy-auth), страница /:

  • 616+ идентичных запросов /api/v1/me + /api/v1/trade-in/quota за ~15 сек, без остановки (834 строки console — все 401). Storm подтверждён, НЕ устранён.
  • GET / в dev-логе всего 3 → это НЕ full-reload loop, а in-place refetch-loop внутри одного маунта.
  • Никаких React-ошибок (Maximum update depth и т.п.), нет setInterval/refetchInterval/invalidateQueries на пути /.

Важно: причина НЕ в retry

useMe уже имеет retry:false на 401 + staleTime/gcTime: Infinity + refetchOnMount:false; useQuota retry:false; QueryClient стабильный (useState). То есть простое выставление retry:false (как предполагал issue) НЕ лечит — петля идёт через повторный mount/refetch, а не через retry. Паттерн запросов me, me, quota (×N) → несколько observer'ов /me (RouteGuard + useActiveBrandSlug + useBrand) ремаунтятся в цикле.

Что нужно

  1. Найти источник ремаунта/рефетча (кандидаты: StrictMode-взаимодействие, observer churn на errored-query, useActiveBrandSlug цепочка). Заглушить → ≤1–2 запроса на стойком 401.
  2. Graceful auth-fail экран вместо белого листа (RouteGuard 401-ветка сейчас рендерит children → монтирует хуки повторно).

Оставляю status/ready — баг реальный, демо-блокер (Praktika 2026-06-01). НЕ закрывать.

## Design-loop repro 2026-05-31 — НЕ чисто, баг живой ❌ Прогон Playwright Network на `next dev` (localhost:3137, backend отвечает 401 без Caddy-auth), страница `/`: - **616+ идентичных запросов `/api/v1/me` + `/api/v1/trade-in/quota`** за ~15 сек, без остановки (834 строки console — все 401). Storm подтверждён, НЕ устранён. - `GET /` в dev-логе всего **3** → это **НЕ full-reload loop**, а **in-place refetch-loop** внутри одного маунта. - Никаких React-ошибок (`Maximum update depth` и т.п.), нет `setInterval`/`refetchInterval`/`invalidateQueries` на пути `/`. ### Важно: причина НЕ в retry `useMe` уже имеет `retry:false` на 401 + `staleTime/gcTime: Infinity` + `refetchOnMount:false`; `useQuota` `retry:false`; QueryClient стабильный (`useState`). То есть **простое выставление `retry:false` (как предполагал issue) НЕ лечит** — петля идёт через повторный mount/refetch, а не через retry. Паттерн запросов `me, me, quota` (×N) → несколько observer'ов `/me` (RouteGuard + useActiveBrandSlug + useBrand) ремаунтятся в цикле. ### Что нужно 1. Найти источник ремаунта/рефетча (кандидаты: StrictMode-взаимодействие, observer churn на errored-query, `useActiveBrandSlug` цепочка). Заглушить → ≤1–2 запроса на стойком 401. 2. Graceful auth-fail экран вместо белого листа (RouteGuard 401-ветка сейчас рендерит children → монтирует хуки повторно). Оставляю `status/ready` — баг реальный, демо-блокер (Praktika 2026-06-01). НЕ закрывать.
Collaborator

Root cause найден (runtime bisection, 2026-05-31)

Бисекция на живом next dev:

  1. Заменил 401-ветку RouteGuard на статический <div> (не монтируя children) → storm исчез: ровно 1 /me, дальше тишина. ⇒ петля целиком в page-subtree (TradeInPage), не в RouteGuard/Providers.

  2. Перехват window.fetch + стектрейс каждого /me:

    fetchMe → query.fetch → QueryObserver.onSubscribe → #executeFetch
             → useSyncExternalStore subscribe → commitHookPassiveMountEffects
    

    Т.е. фетч инициирует подписка нового observer'а (onSubscribe), а не retry/refetchInterval.

Механизм

  • /me в error-state (401). В TanStack запрос без успешных данных всегда stalestaleTime:Infinity к ошибкам НЕ применяется. ⇒ каждая подписка observer'а рефетчит (onSubscribe→fetch), несмотря на refetchOnMount:false/retry:false.
  • Page-subtree под 401 непрерывно ре-подписывается (re-render/re-subscribe churn) ⇒ subscribe→refetch бесконечно.
  • В prod с валидной auth /me=200 (fresh, не stale) ⇒ подписка НЕ рефетчит ⇒ шторма нет. Шторм воспроизводится только на стойком 401 / истёкшей сессии / offline.

Почему retry:false (как предлагал issue) не лечит

Это не retry — это subscribe-on-stale-error. Лечится двумя независимыми рычагами:

  • (A, рекомендую) RouteGuard: на стойкий 401 в prod рендерить graceful "сессия истекла / войти" вместо монтирования app → subtree не монтируется → нет re-subscribe loop → ≤1 запрос + внятный экран (закрывает обе половины DoD). Нюанс: сохранить dev escape-hatch (локально без Caddy 401 — норма). Через process.env.NODE_ENV → dev passthrough, prod screen.
  • (B) Починить сам re-render/re-subscribe loop в TablInPage (тогда и dev-repro npm run dev без бэка перестанет штормить). Источник churn пока не локализован до строки — нужен ещё проход (кандидаты: нестабильные deps/ключи в always-mounted детях Topbar/EstimateForm/quota-banner).

Решение по (A)-vs-(B) и поведению dev escape-hatch — за владельцем (затрагивает auth-gate + локальный dev-workflow команды). Готов реализовать любой вариант.

## Root cause найден (runtime bisection, 2026-05-31) Бисекция на живом `next dev`: 1. **Заменил 401-ветку RouteGuard на статический `<div>`** (не монтируя children) → storm **исчез**: ровно **1** `/me`, дальше тишина. ⇒ петля целиком в **page-subtree** (`TradeInPage`), не в RouteGuard/Providers. 2. **Перехват `window.fetch` + стектрейс** каждого `/me`: ``` fetchMe → query.fetch → QueryObserver.onSubscribe → #executeFetch → useSyncExternalStore subscribe → commitHookPassiveMountEffects ``` Т.е. фетч инициирует **подписка нового observer'а** (`onSubscribe`), а не retry/refetchInterval. ### Механизм - `/me` в **error-state** (401). В TanStack запрос без успешных данных **всегда stale** — `staleTime:Infinity` к ошибкам НЕ применяется. ⇒ **каждая подписка observer'а рефетчит** (`onSubscribe`→fetch), несмотря на `refetchOnMount:false`/`retry:false`. - Page-subtree под 401 **непрерывно ре-подписывается** (re-render/re-subscribe churn) ⇒ subscribe→refetch бесконечно. - **В prod с валидной auth** `/me`=200 (fresh, не stale) ⇒ подписка НЕ рефетчит ⇒ **шторма нет**. Шторм воспроизводится только на стойком 401 / истёкшей сессии / offline. ### Почему `retry:false` (как предлагал issue) не лечит Это не retry — это subscribe-on-stale-error. Лечится двумя независимыми рычагами: - **(A, рекомендую) RouteGuard: на стойкий 401 в prod рендерить graceful "сессия истекла / войти" вместо монтирования app** → subtree не монтируется → нет re-subscribe loop → ≤1 запрос + внятный экран (закрывает обе половины DoD). **Нюанс:** сохранить dev escape-hatch (локально без Caddy 401 — норма). Через `process.env.NODE_ENV` → dev passthrough, prod screen. - **(B) Починить сам re-render/re-subscribe loop** в `TablInPage` (тогда и dev-repro `npm run dev` без бэка перестанет штормить). Источник churn пока не локализован до строки — нужен ещё проход (кандидаты: нестабильные deps/ключи в always-mounted детях Topbar/EstimateForm/quota-banner). **Решение по (A)-vs-(B) и поведению dev escape-hatch — за владельцем** (затрагивает auth-gate + локальный dev-workflow команды). Готов реализовать любой вариант.
Owner

делаем (A, рекомендую)

делаем (A, рекомендую)
bot-frontend self-assigned this 2026-05-31 13:04:55 +00:00
bot-frontend added the
status/wip
label 2026-05-31 13:04:55 +00:00
bot-frontend added
status/review
and removed
status/ready
status/wip
labels 2026-05-31 13:08:08 +00:00
lekss361 added
status/qa
and removed
status/review
labels 2026-06-06 18:43:18 +00:00
Owner

Status actualize (06.06): влито в main через PR #896 (commit 191ed03, RouteGuard 401 → session-expired, stop retry-storm). Открытых PR нет. status/reviewstatus/qa.

Status actualize (06.06): влито в `main` через PR #896 (commit `191ed03`, RouteGuard 401 → session-expired, stop retry-storm). Открытых PR нет. `status/review` → `status/qa`.
Sign in to join this conversation.
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#800
No description provided.