search_by_quarter (nspd_bulk_client.py:305-316): подстрочная классификация 404/400 по тексту исключения с подмешанным body_preview проглатывает 5xx и валидный 400 как «кв… #1626

Closed
opened 2026-06-16 18:44:18 +00:00 by bot-backend · 0 comments
Collaborator

Severity: medium · Категория: silent-except / data-loss · Файл: backend/app/scrapers/nspd_bulk_client.py:305-316

Файл: backend/app/scrapers/nspd_bulk_client.py:305-316.

Проблема: в except-ветке search_by_quarter решение вернуть пустой QuarterSnapshot принимается по err_str = str(e); if "HTTP 404" in err_str or "HTTP 400" in err_str. except перехватывает базовый NspdBulkError, под который подпадают ВСЕ подклассы, включая NspdBulkServerError (5xx). Текст исключения для 5xx собирается в _get_json:229-231 как f"HTTP {code} server error: {url} — {body_preview}", для 4xx — :234-236 f"HTTP {code}: {url} — {body_preview}", где body_preview = resp.text[:300] (сырое тело ответа NSPD, HTML/JSON-страница ошибки).

Два дефекта:

  1. Хрупкая классификация: если тело реального 5xx содержит литерал 'HTTP 404' или 'HTTP 400' в первых 300 символах, ошибка ложно классифицируется как «квартал не найден». Вместо проброса NspdBulkServerError возвращается пустой snapshot.
  2. Валидный HTTP 400 безусловно трактуется как «не найдено» независимо от тела. 400 обычно означает невалидный запрос (наш баг формирования query/params), а не отсутствие данных.

Импакт: caller bulk_harvest.py:130 вызывает search_by_quarter без обёртки try/except, полагаясь на проброс NspdBulkServerError до Celery-таска bulk_harvest_quarter (scrape_cadastre.py:141 autoretry_for=(Exception,), :144 dont_autoretry_for=(NspdBulkWafError,)) для ретрая всего квартала (контракт issue #252). Пустой snapshot подавляет исключение → квартал считается успешно собранным с features=[] → молчаливая потеря данных целого квартала и ложь в отчёте о покрытии. Для 400 — дополнительно маскирует реальные дефекты формирования запроса.

Фикс: классифицировать по ТИПУ исключения / коду статуса, а не по подстроке в тексте. Например: хранить status_code как атрибут на NspdBulkError (raise NspdBulkError(..., status_code=code)) и в except проверять e.status_code == 404; ловить только not-found-семантику (404), а NspdBulkServerError/NspdBulkRateLimitError пробрасывать (поймать NspdBulkServerError отдельно ДО общего NspdBulkError или исключить через isinstance). HTTP 400 не трактовать как empty — пробрасывать как ошибку запроса.

Почему баг: Два дефекта в одной точке. (1) Хрупкая подстрочная классификация: статус-код извлекается из произвольного текста, в который подмешано неконтролируемое тело ответа сервера → 5xx может выглядеть как 404. При этом ломается контракт issue #252 — caller (bulk_harvest_quarter_task) должен ретраить весь квартал при NspdBulkServerError, но пустой snapshot подавляет исключение, квартал помечается как успешно собранный с features=[] → молчаливая потеря данных целого квартала, ложь в отчёте о покрытии. (2) Даже корректный HTTP 400 трактуется как «квартал не найден»: 400 обычно означает невалидный запрос (битый query/параметры — наш баг), а не отсутствие данных; проглатывание 400 как empty маскирует реальные дефекты формирования запроса и тоже даёт тихую потерю.

Как чинить: Не разбирать статус по str(e). Прокинуть HTTP-статус как атрибут исключения (например self-поле в NspdBulkError/_get_json) и сверять числовой код: возвращать пустой snapshot только при реальном 404; 400 логировать как ошибку запроса и пробрасывать; 5xx/ServiceException обязательно пробрасывать наверх, чтобы сработал quarter-retry #252.


deep-audit backend v2 (2-й проход, после PR #1543) · verify+harden, confidence 0.8 · unit B11

**Severity:** medium · **Категория:** silent-except / data-loss · **Файл:** `backend/app/scrapers/nspd_bulk_client.py:305-316` Файл: backend/app/scrapers/nspd_bulk_client.py:305-316. Проблема: в except-ветке search_by_quarter решение вернуть пустой QuarterSnapshot принимается по `err_str = str(e); if "HTTP 404" in err_str or "HTTP 400" in err_str`. except перехватывает базовый NspdBulkError, под который подпадают ВСЕ подклассы, включая NspdBulkServerError (5xx). Текст исключения для 5xx собирается в _get_json:229-231 как `f"HTTP {code} server error: {url} — {body_preview}"`, для 4xx — :234-236 `f"HTTP {code}: {url} — {body_preview}"`, где body_preview = resp.text[:300] (сырое тело ответа NSPD, HTML/JSON-страница ошибки). Два дефекта: 1. Хрупкая классификация: если тело реального 5xx содержит литерал 'HTTP 404' или 'HTTP 400' в первых 300 символах, ошибка ложно классифицируется как «квартал не найден». Вместо проброса NspdBulkServerError возвращается пустой snapshot. 2. Валидный HTTP 400 безусловно трактуется как «не найдено» независимо от тела. 400 обычно означает невалидный запрос (наш баг формирования query/params), а не отсутствие данных. Импакт: caller bulk_harvest.py:130 вызывает search_by_quarter без обёртки try/except, полагаясь на проброс NspdBulkServerError до Celery-таска bulk_harvest_quarter (scrape_cadastre.py:141 autoretry_for=(Exception,), :144 dont_autoretry_for=(NspdBulkWafError,)) для ретрая всего квартала (контракт issue #252). Пустой snapshot подавляет исключение → квартал считается успешно собранным с features=[] → молчаливая потеря данных целого квартала и ложь в отчёте о покрытии. Для 400 — дополнительно маскирует реальные дефекты формирования запроса. Фикс: классифицировать по ТИПУ исключения / коду статуса, а не по подстроке в тексте. Например: хранить status_code как атрибут на NspdBulkError (raise NspdBulkError(..., status_code=code)) и в except проверять `e.status_code == 404`; ловить только not-found-семантику (404), а NspdBulkServerError/NspdBulkRateLimitError пробрасывать (поймать NspdBulkServerError отдельно ДО общего NspdBulkError или исключить через isinstance). HTTP 400 не трактовать как empty — пробрасывать как ошибку запроса. **Почему баг:** Два дефекта в одной точке. (1) Хрупкая подстрочная классификация: статус-код извлекается из произвольного текста, в который подмешано неконтролируемое тело ответа сервера → 5xx может выглядеть как 404. При этом ломается контракт issue #252 — caller (bulk_harvest_quarter_task) должен ретраить весь квартал при NspdBulkServerError, но пустой snapshot подавляет исключение, квартал помечается как успешно собранный с features=[] → молчаливая потеря данных целого квартала, ложь в отчёте о покрытии. (2) Даже корректный HTTP 400 трактуется как «квартал не найден»: 400 обычно означает невалидный запрос (битый query/параметры — наш баг), а не отсутствие данных; проглатывание 400 как empty маскирует реальные дефекты формирования запроса и тоже даёт тихую потерю. **Как чинить:** Не разбирать статус по str(e). Прокинуть HTTP-статус как атрибут исключения (например self-поле в NspdBulkError/_get_json) и сверять числовой код: возвращать пустой snapshot только при реальном 404; 400 логировать как ошибку запроса и пробрасывать; 5xx/ServiceException обязательно пробрасывать наверх, чтобы сработал quarter-retry #252. --- <sub>deep-audit backend v2 (2-й проход, после PR #1543) · verify+harden, confidence 0.8 · unit B11</sub>
bot-backend added the
week ревью 1
label 2026-06-16 18:44:18 +00:00
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#1626
No description provided.