(high) useSiteAnalysis: re-POST на 'ready' использует apiFetch без проверки статуса — 202-гонка возвращает stub и крашит render tree #1339

Closed
opened 2026-06-14 18:08:40 +00:00 by bot-backend · 1 comment
Collaborator

Описание

После того как /fetch-status вернул status==='ready', хук повторно POST-ит /analyze через обычный apiFetch<ParcelAnalysis>, который возвращает JSON-тело при ЛЮБОМ 2xx — включая 202 'Accepted' stub. Бэкенд может ответить 202 на re-POST (задокументированная гонка: геометрия помечена ready, но analyze-воркер не закончил). Канонический useParcelAnalyzeQuery обрабатывает это (re-check статуса и продолжение polling: «If it races back to 202, keep polling rather than returning the stub»), но mutation-хук здесь — нет. Когда 202-stub (AnalyzeAcceptedResponse: {status,cad_num,job_id,eta_seconds,message} — без score/competitors/score_breakdown) возвращается как ParcelAnalysis, рендер Секций 3/4 читает data.score / data.score_breakdown на undefined и крашит дерево компонентов.

Корень

apiFetch<T> (api.ts:38) бросает исключение только при !response.ok (4xx/5xx). Статус 202 имеет response.ok === true, поэтому тело 202 молча возвращается кастованным как ParcelAnalysis. Первый POST (строки 119-133) правильно использует apiFetchWithStatus + проверку first.status === 200; второй POST после ready — нет.

Evidence

useSiteAnalysis.ts:166-181:

const second = await apiFetch<ParcelAnalysis>(analyzeUrl(cad), {
  method: "POST",
  signal,
  ...(bodyPayload ? { body: bodyPayload, headers: { "Content-Type": "application/json" } } : {}),
});
...
return second;   // строка 181 — нет проверки статуса, 202-stub возвращается дословно

Suggested fix

Зеркалить useParcelAnalyzeQuery: использовать apiFetchWithStatus<ParcelAnalysis | AnalyzeAcceptedResponse>, затем if (second.status === 200) return second.body as ParcelAnalysis;, иначе продолжать polling-цикл вместо возврата 202-тела.

Обоснование severity

high: редкая гонка, но приводит к крашу render tree для пользователя без авто-восстановления (нужен новый запрос). confidence 0.92.


week ревью 1 · multi-agent аудит 2026-06-14 · severity=high · category=async-concurrency

Затронутые файлы:

  • /Users/anton/Птица/gendesign/frontend/src/hooks/useSiteAnalysis.ts
## Описание После того как `/fetch-status` вернул `status==='ready'`, хук повторно POST-ит `/analyze` через обычный `apiFetch<ParcelAnalysis>`, который возвращает JSON-тело при ЛЮБОМ 2xx — включая 202 'Accepted' stub. Бэкенд может ответить 202 на re-POST (задокументированная гонка: геометрия помечена ready, но analyze-воркер не закончил). Канонический `useParcelAnalyzeQuery` обрабатывает это (re-check статуса и продолжение polling: «If it races back to 202, keep polling rather than returning the stub»), но mutation-хук здесь — нет. Когда 202-stub (`AnalyzeAcceptedResponse`: `{status,cad_num,job_id,eta_seconds,message}` — без `score`/`competitors`/`score_breakdown`) возвращается как `ParcelAnalysis`, рендер Секций 3/4 читает `data.score` / `data.score_breakdown` на undefined и крашит дерево компонентов. ## Корень `apiFetch<T>` (api.ts:38) бросает исключение только при `!response.ok` (4xx/5xx). Статус 202 имеет `response.ok === true`, поэтому тело 202 молча возвращается кастованным как `ParcelAnalysis`. Первый POST (строки 119-133) правильно использует `apiFetchWithStatus` + проверку `first.status === 200`; второй POST после ready — нет. ## Evidence `useSiteAnalysis.ts:166-181`: ```ts const second = await apiFetch<ParcelAnalysis>(analyzeUrl(cad), { method: "POST", signal, ...(bodyPayload ? { body: bodyPayload, headers: { "Content-Type": "application/json" } } : {}), }); ... return second; // строка 181 — нет проверки статуса, 202-stub возвращается дословно ``` ## Suggested fix Зеркалить `useParcelAnalyzeQuery`: использовать `apiFetchWithStatus<ParcelAnalysis | AnalyzeAcceptedResponse>`, затем `if (second.status === 200) return second.body as ParcelAnalysis;`, иначе продолжать polling-цикл вместо возврата 202-тела. ## Обоснование severity high: редкая гонка, но приводит к крашу render tree для пользователя без авто-восстановления (нужен новый запрос). confidence 0.92. --- _week ревью 1 · multi-agent аудит 2026-06-14 · severity=**high** · category=`async-concurrency`_ Затронутые файлы: - `/Users/anton/Птица/gendesign/frontend/src/hooks/useSiteAnalysis.ts`
bot-backend added the
week ревью 1
label 2026-06-14 18:08:40 +00:00
bot-backend added the
scope/backend
label 2026-06-15 11:01:49 +00:00
Owner

🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)

Текущее состояние кода: frontend/src/hooks/useSiteAnalysis.ts:190-205 (re-POST после ready через apiFetch<ParcelAnalysis> + return second без проверки статуса). Первый POST корректен — apiFetchWithStatus + if (first.status === 200) на lines 143-158.

Корень: apiFetch<T> бросает только при !response.ok (4xx/5xx); 202 имеет response.ok === true, поэтому 202-тело молча кастуется как ParcelAnalysis и возвращается из return second (line 205). На re-POST после status === "ready" бэкенд может ответить 202 (задокументированная гонка: геометрия ready, analyze-воркер не закончил → AnalyzeAcceptedResponse без score/score_breakdown/competitors). Канонический useParcelAnalyzeQuery это обрабатывает — lib/site-finder-api.ts:451-457: re-POST через apiFetchWithStatus, if (second.status === 200) return ... иначе «202 race → fall through and poll again». В mutation-хуке этой симметрии нет.

Что править:

  • Зеркалить useParcelAnalyzeQuery: заменить apiFetch<ParcelAnalysis> (useSiteAnalysis.ts:190) на apiFetchWithStatus<ParcelAnalysis | AnalyzeAcceptedResponse>; затем if (second.status === 200) { if (!signal.aborted) setFetchingState(null); return second.body as ParcelAnalysis; }, иначе НЕ возвращать тело, а continue цикл polling (не выходить из for-loop). Сохранить существующий abort-guard (#1242).

Что посмотреть:

  • Код: hooks/useSiteAnalysis.ts:184-205 (правка), эталон lib/site-finder-api.ts:442-457; тест — re-POST возвращает 202 → хук продолжает poll, не крашит. (Затронутый файл в теле — /Users/anton/Птица/... — это абсолютный путь автора аудита; canonical в репо frontend/src/hooks/useSiteAnalysis.ts.)
  • Vault: релевантных заметок не найдено (смежно: комментарий в коде lib/site-finder-api.ts:407-410 про #1001/958-B5 — тот же класс бага в query-хуке, уже исправлен).
**🔍 week ревью 1 — верификация (ПОДТВЕРЖДЕНО)** **Текущее состояние кода:** `frontend/src/hooks/useSiteAnalysis.ts:190-205` (re-POST после ready через `apiFetch<ParcelAnalysis>` + `return second` без проверки статуса). Первый POST корректен — `apiFetchWithStatus` + `if (first.status === 200)` на lines 143-158. **Корень:** `apiFetch<T>` бросает только при `!response.ok` (4xx/5xx); 202 имеет `response.ok === true`, поэтому 202-тело молча кастуется как `ParcelAnalysis` и возвращается из `return second` (line 205). На re-POST после `status === "ready"` бэкенд может ответить 202 (задокументированная гонка: геометрия ready, analyze-воркер не закончил → `AnalyzeAcceptedResponse` без `score`/`score_breakdown`/`competitors`). Канонический `useParcelAnalyzeQuery` это обрабатывает — `lib/site-finder-api.ts:451-457`: re-POST через `apiFetchWithStatus`, `if (second.status === 200) return ...` иначе «202 race → fall through and poll again». В mutation-хуке этой симметрии нет. **Что править:** - Зеркалить `useParcelAnalyzeQuery`: заменить `apiFetch<ParcelAnalysis>` (useSiteAnalysis.ts:190) на `apiFetchWithStatus<ParcelAnalysis | AnalyzeAcceptedResponse>`; затем `if (second.status === 200) { if (!signal.aborted) setFetchingState(null); return second.body as ParcelAnalysis; }`, иначе НЕ возвращать тело, а `continue` цикл polling (не выходить из for-loop). Сохранить существующий abort-guard (#1242). **Что посмотреть:** - Код: `hooks/useSiteAnalysis.ts:184-205` (правка), эталон `lib/site-finder-api.ts:442-457`; тест — re-POST возвращает 202 → хук продолжает poll, не крашит. (Затронутый файл в теле — `/Users/anton/Птица/...` — это абсолютный путь автора аудита; canonical в репо `frontend/src/hooks/useSiteAnalysis.ts`.) - Vault: релевантных заметок не найдено (смежно: комментарий в коде `lib/site-finder-api.ts:407-410` про #1001/958-B5 — тот же класс бага в query-хуке, уже исправлен).
Sign in to join this conversation.
No milestone
No project
No assignees
2 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#1339
No description provided.