[Fable 5][uncertain][P3] Trade-in: чтение window.location.search во время рендера — hydration mismatch при открытии shareable-ссылки ?id= #1247

Closed
opened 2026-06-13 04:58:13 +00:00 by bot-backend · 1 comment
Collaborator

Из армейского ревью Claude Fable 5 (2026-06-11) — UNCERTAIN (1/2 верификаторов real)

⚠️ Только 1 из 2 адверсариальных верификаторов подтвердил баг как real. Заведено по решению Anton (2026-06-13) — требует дополнительной проверки перед фиксом. Severity P3 (correctness), confidence 0.55.

Файл

frontend/src/app/trade-in/page.tsx:19

Evidence (цитата кода)

function useEstimateId() {
  // SSR guard: searchParams is only available in client
  if (typeof window === "undefined") return null;
  const params = new URLSearchParams(window.location.search);
  return params.get("id");
}

Why bug (по версии finder'а)

Страница пререндерится статически: серверный HTML строится с urlEstimateId=null → ветка EmptyState. При гидрации по ссылке /trade-in?id= первый клиентский рендер получает id, useEstimate включается (isLoading=true) и рендерит EstimateProgress — дерево не совпадает с серверным HTML → React 19 hydration error, полный клиентский ре-рендер (flash + ошибка в консоли/GlitchTip). В legacy-site-finder то же решено правильно — useSearchParams() под Suspense.

Suggested fix

Использовать useSearchParams() из next/navigation с Suspense-boundary (как в legacy/site-finder/page.tsx), либо читать id в useEffect и класть в useState.

Verify notes (вердикты адверсариальных верификаторов)

  • [real/P3] "use client" страница без dynamic → пререндер: window undefined → id=null → useEstimate disabled → HTML = EmptyState. Открытие /trade-in?id= (ссылку создаёт router.replace, стр.48): hydration-рендер читает window.location.search → useEstimate enabled, кэш пуст → isLoading=true → EstimateProgress вместо EmptyState → React 19 hydration mismatch, re-render корня + ошибка в консоли. Legacy site-finder решает это через Suspense+useSearchParams. Восстановимо → P3.
  • [refuted/P3] RouteGuard в root layout гейтит все страницы: useMe() — useQuery без initialData на свежем QueryClient ⇒ и на сервере, и в первом клиентском рендере guard возвращает null (isLoading/!data). EmptyState/EstimateProgress не попадают в серверный HTML; TradeInPage монтируется после ответа /me — пост-гидрационный клиентский рендер, где window.location.search безопасен. В проде /trade-in вообще отдаёт tradein-mvp bundle (Caddy → tradein-frontend) с зеркальным guard'ом. Mismatch невозможен.

Provenance

  • Finders: fe-app-lib
  • Confidence: 0.55
  • Verifier votes: 1 real / 1 refuted (на основании 2 попыток)
  • Originally batched at: main@d796027
## Из армейского ревью Claude Fable 5 (2026-06-11) — UNCERTAIN (1/2 верификаторов real) ⚠️ **Только 1 из 2 адверсариальных верификаторов подтвердил баг как `real`**. Заведено по решению Anton (2026-06-13) — требует **дополнительной проверки** перед фиксом. Severity **P3** (correctness), confidence 0.55. ## Файл [`frontend/src/app/trade-in/page.tsx:19`](frontend/src/app/trade-in/page.tsx:19) ## Evidence (цитата кода) ``` function useEstimateId() { // SSR guard: searchParams is only available in client if (typeof window === "undefined") return null; const params = new URLSearchParams(window.location.search); return params.get("id"); } ``` ## Why bug (по версии finder'а) Страница пререндерится статически: серверный HTML строится с urlEstimateId=null → ветка EmptyState. При гидрации по ссылке /trade-in?id=<uuid> первый клиентский рендер получает id, useEstimate включается (isLoading=true) и рендерит EstimateProgress — дерево не совпадает с серверным HTML → React 19 hydration error, полный клиентский ре-рендер (flash + ошибка в консоли/GlitchTip). В legacy-site-finder то же решено правильно — useSearchParams() под Suspense. ## Suggested fix Использовать useSearchParams() из next/navigation с Suspense-boundary (как в legacy/site-finder/page.tsx), либо читать id в useEffect и класть в useState. ## Verify notes (вердикты адверсариальных верификаторов) - [real/P3] "use client" страница без dynamic → пререндер: window undefined → id=null → useEstimate disabled → HTML = EmptyState. Открытие /trade-in?id=<uuid> (ссылку создаёт router.replace, стр.48): hydration-рендер читает window.location.search → useEstimate enabled, кэш пуст → isLoading=true → EstimateProgress вместо EmptyState → React 19 hydration mismatch, re-render корня + ошибка в консоли. Legacy site-finder решает это через Suspense+useSearchParams. Восстановимо → P3. - [refuted/P3] RouteGuard в root layout гейтит все страницы: useMe() — useQuery без initialData на свежем QueryClient ⇒ и на сервере, и в первом клиентском рендере guard возвращает null (isLoading/!data). EmptyState/EstimateProgress не попадают в серверный HTML; TradeInPage монтируется после ответа /me — пост-гидрационный клиентский рендер, где window.location.search безопасен. В проде /trade-in вообще отдаёт tradein-mvp bundle (Caddy → tradein-frontend) с зеркальным guard'ом. Mismatch невозможен. ## Provenance - Finders: `fe-app-lib` - Confidence: 0.55 - Verifier votes: 1 real / 1 refuted (на основании 2 попыток) - Originally batched at: main@d796027
bot-backend added the
Fable 5 ревью
label 2026-06-13 04:58:13 +00:00
Author
Collaborator

Verify-вердикт: NOT-REAL (uncertain 1/2 подтвердился как false-positive на observable behavior).

useEstimateId() действительно читает window.location.search во время рендера, НО hydration mismatch не возникает: layout.tsx оборачивает страницу в client-only RouteGuard, который if (isLoading) return null на uncached useMe() (TanStack useQuery без server-prefetch/HydrationBoundary/persist — grep-подтверждено). Поэтому:

  • SSR prerender: me uncached → isLoading=true → RouteGuard → null (тело страницы НЕ в HTML)
  • Первый client-render (hydration): me uncached → isLoading=true → null
  • Оба = null → React сверяет чисто → mismatch-warning не фейрит.

TradeInPage/useEstimateId монтируются только ПОСЛЕ резолва /me — это уже post-hydration client-render, чтение window там безопасно (нет SSR-counterpart для расхождения).

Verifier флагнул по локальному паттерну («читает window в рендере + ветвит UI под статик-пререндер») не протрассировав layout→RouteGuard→useMe гейт. True-positive на паттерн, false-positive на поведение.

Закрываю как not-reproduced. Опциональный hardening (useSearchParams/useEffect — на случай если страницу когда-то отрендерят вне guard) — не делаю, churn для non-bug; при желании отдельной robustness-задачей.

**Verify-вердикт: NOT-REAL** (uncertain 1/2 подтвердился как false-positive на observable behavior). `useEstimateId()` действительно читает `window.location.search` во время рендера, НО hydration mismatch **не возникает**: `layout.tsx` оборачивает страницу в client-only `RouteGuard`, который `if (isLoading) return null` на uncached `useMe()` (TanStack useQuery без server-prefetch/HydrationBoundary/persist — grep-подтверждено). Поэтому: - SSR prerender: `me` uncached → isLoading=true → RouteGuard → `null` (тело страницы НЕ в HTML) - Первый client-render (hydration): `me` uncached → isLoading=true → `null` - Оба = `null` → React сверяет чисто → mismatch-warning не фейрит. `TradeInPage`/`useEstimateId` монтируются только ПОСЛЕ резолва `/me` — это уже post-hydration client-render, чтение window там безопасно (нет SSR-counterpart для расхождения). Verifier флагнул по локальному паттерну («читает window в рендере + ветвит UI под статик-пререндер») не протрассировав layout→RouteGuard→useMe гейт. True-positive на паттерн, false-positive на поведение. Закрываю как not-reproduced. Опциональный hardening (useSearchParams/useEffect — на случай если страницу когда-то отрендерят вне guard) — не делаю, churn для non-bug; при желании отдельной robustness-задачей.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#1247
No description provided.