Отказ ручного сбора каталога DOM.РФ больше не советует ждать cooldown (#2443)

Ревью PR #3566: обещание таймера, убранное из beat, осталось в тексте
HTTP 400 эндпоинтов /admin/scrape/kn-catalog-objects и /kn-catalog-flats.
Отказ предлагал передать i_understand_waf_risk=true, «если WAF cooldown
прошёл», то есть подсказывал оператору обойти блокировку, которую ожидание
не снимает (зонды 20.08-01.09: StormWall, «Доступ заблокирован [403]»).

Теперь отказ называет StormWall и реальное условие: прокси и kn-прогон,
принятый по числу строк (#3307). Константа переименована в
_DOMRF_BLOCK_GUARD_MSG. Докстринги эндпоинтов и задач
scrape_kn_catalog_flats/objects больше не говорят про WAF cooldown и про
«вторник 04:00 UTC» у выключенной записи с расписанием в МСК.
api-types.ts перегенерирован как в CI-гейте openapi-codegen-check,
изменились только два докстринга.

Тесты отказа проверяют значение detail: #3307 есть, cooldown нет.
Поведение guard'а (400 без флага, задача не ставится) не менялось.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
bot-backend 2026-09-17 13:58:31 +05:00
parent b2ff92afbb
commit 7cd15aab71
5 changed files with 55 additions and 46 deletions

View file

@ -1910,17 +1910,19 @@ def trigger_ekburg_permits(
return {"task_id": result.id, "scope": scope, "queued_at": "now"}
# WAF cooldown guard message (#2443 — DOM.РФ hard-banned this VPS's IP 2026-05-24
# после серии failed catalog SSR extras-сессий). Beat schedule для catalog-object
# и catalog-flat scrape'ов ОТКЛЮЧЕН по этой же причине (см. beat_schedule.py) —
# оба ad-hoc admin-эндпоинта ниже бьют по ТОМУ ЖЕ /сервисы/* BrowserSession
# path family, поэтому без явного оператор-override могут углубить бан (#2445 D1).
_WAF_COOLDOWN_GUARD_MSG = (
"Ad-hoc catalog-scrape заблокирован guard'ом: DOM.РФ WAF hard-ban этого VPS IP "
"2026-05-24 (issue #2443), beat schedule для этого таска отключён по той же "
"причине. Повторный ad-hoc запуск может углубить бан. Если ты осознанно "
"принимаешь этот риск (WAF cooldown прошёл, targeted smoke-test и т.п.) — "
"передай i_understand_waf_risk=true в теле запроса."
# Текст отказа guard'а блокировки DOM.РФ (#2443, #2445 D1). Зонды 20.08, 27.08 и
# 01.09: наш.дом.рф за StormWall отдаёт серверу «Доступ заблокирован [403]», таймера
# у этой блокировки нет. Beat-записи catalog-object и catalog-flat scrape'ов
# выключены по той же причине (см. beat_schedule.py); оба ad-hoc эндпоинта ниже
# ходят тем же /сервисы/* BrowserSession path family. Оператор решает по этому
# тексту, ставить ли флаг, поэтому он называет реальное условие (#3307).
_DOMRF_BLOCK_GUARD_MSG = (
"Ad-hoc catalog-scrape заблокирован guard'ом: наш.дом.рф за StormWall отдаёт "
"этому серверу «Доступ заблокирован [403]» (зонды 20.08-01.09, issue #2443), "
"beat schedule для этого таска выключен по той же причине. Ожидание блокировку "
"не снимает, запуск отсюда даст только отказы. Передавай "
"i_understand_waf_risk=true, только когда сбор идёт через прокси и kn-прогон "
"принят по числу строк (#3307)."
)
@ -1952,20 +1954,20 @@ def trigger_kn_catalog_objects(
"""Manual trigger для catalog-OBJECT scraper (заполняет wall_type, energy_eff,
ceiling_height_m, parking_*, playground_*, scores из SSR __NEXT_DATA__).
Beat schedule: Tuesday 04:00 UTC, batch 300/run. Этот endpoint для ad-hoc
запуска (smoke-тест после деплоя или повторный pass для свежесозданных
объектов до next beat fire).
Beat-запись выключена (блокировка StormWall, #2443; см. beat_schedule.py).
Этот endpoint для ad-hoc запуска (smoke-тест или повторный pass для
свежесозданных объектов).
- max_objects=None дефолтный лимит таска (300).
- max_objects=3 smoke-тест.
- force=True "Загрузить все": игнорирует skip-today, грузит всё подряд.
WAF cooldown guard (#2443, #2445 D1): требует i_understand_waf_risk=true —
beat schedule для этого таска отключён из-за WAF hard-ban 2026-05-24, ad-hoc
re-trigger без явного подтверждения оператора запрещён.
Guard блокировки DOM.РФ (#2443, #2445 D1): требует i_understand_waf_risk=true —
beat schedule для этого таска выключен из-за блокировки StormWall, ad-hoc
запуск без явного подтверждения оператора запрещён (условие #3307).
"""
if not payload.i_understand_waf_risk:
raise HTTPException(status_code=400, detail=_WAF_COOLDOWN_GUARD_MSG)
raise HTTPException(status_code=400, detail=_DOMRF_BLOCK_GUARD_MSG)
from app.workers.tasks.scrape_kn_catalog_objects import scrape_kn_catalog_objects
@ -2022,12 +2024,12 @@ def trigger_kn_catalog_flats(
- max_flats=3 smoke-тест.
- force=True 'Загрузить все': игнорирует фильтр свежести, грузит всё с hash.
WAF cooldown guard (#2443, #2445 D1): требует i_understand_waf_risk=true —
same /сервисы/* BrowserSession path family как catalog-objects, риск re-trigger
того же WAF-бана.
Guard блокировки DOM.РФ (#2443, #2445 D1): требует i_understand_waf_risk=true —
same /сервисы/* BrowserSession path family как catalog-objects, закрыт той же
блокировкой StormWall (условие #3307).
"""
if not payload.i_understand_waf_risk:
raise HTTPException(status_code=400, detail=_WAF_COOLDOWN_GUARD_MSG)
raise HTTPException(status_code=400, detail=_DOMRF_BLOCK_GUARD_MSG)
from app.workers.tasks.scrape_kn_catalog_flats import scrape_kn_catalog_flats

View file

@ -24,8 +24,9 @@ Sequencing dependency (важно): эта задача НЕ дублирует
1. #2442 Task 1 (elemId → catalog_url_hash) задеплоен, И
2. прошёл свежий kn-sweep, наполнивший catalog_url_hash на новых snapshot'ах.
До этого SELECT вернёт 0 строк (см. выше). Beat-entry намеренно оставлен
ЗАКОММЕНТИРОВАННЫМ в beat_schedule.py (как и object-level) WAF-cooldown на
VPS IP; включается вручную после проверки targeted-тестом.
ЗАКОММЕНТИРОВАННЫМ в beat_schedule.py (как и object-level): наш.дом.рф за
StormWall отдаёт «Доступ заблокирован [403]» (#2443), ожидание это не снимает;
включать после прокси и принятого kn-прогона (#3307).
"""
from __future__ import annotations
@ -116,7 +117,7 @@ def scrape_kn_catalog_flats(
Concurrency:
No Redis lock consistent с sibling tasks (scrape_kn_catalog_objects и т.д.).
Beat отключён (WAF cooldown); при ручном/ad-hoc запуске пересечение крайне
Beat отключён (блокировка StormWall, #2443); при ручном/ad-hoc запуске пересечение крайне
маловероятно. Если случится:
- UPDATE идемпотентен (COALESCE в upsert_catalog_data, WHERE ods_id).
- Max risk: 2x WAF-нагрузка на DOM.РФ для того же batch.

View file

@ -9,7 +9,7 @@ Selector logic:
- DATE(catalog_scraped_at) = CURRENT_DATE уже сегодня обновлён, пропускаем
- force=True игнорирует фильтр, загружает все объекты последнего snapshot
Beat schedule: вторник 04:00 UTC (в beat_schedule.py).
Beat-запись (вторник 04:00 МСК) выключена в beat_schedule.py: блокировка StormWall (#2443).
"""
from __future__ import annotations

View file

@ -1,10 +1,11 @@
"""Тесты для WAF cooldown guard (#2443, #2445 D1) на ad-hoc catalog-scrape эндпоинтах.
"""Тесты guard'а блокировки DOM.РФ (#2443, #2445 D1) на ad-hoc catalog-scrape эндпоинтах.
POST /api/v1/admin/scrape/kn-catalog-objects и /kn-catalog-flats бьют по тому же
/сервисы/* BrowserSession path family, что вызвал DOM.РФ WAF hard-ban 2026-05-24
(#2443). Beat schedule для обоих тасков отключён по этой причине — эти эндпоинты
должны отказывать в запуске БЕЗ явного i_understand_waf_risk=true, чтобы случайный
smoke-тест/re-trigger не углубил бан.
/сервисы/* BrowserSession path family, что закрыт StormWall (#2443). Beat schedule
для обоих тасков отключён по этой причине эти эндпоинты должны отказывать в
запуске БЕЗ явного i_understand_waf_risk=true. Текст отказа то, по чему оператор
решает, ставить ли флаг, поэтому он обязан называть реальное условие (#3307), а не
ожидание таймера: зонды 20.08-01.09 показали, что ждать нечего.
"""
from __future__ import annotations
@ -19,6 +20,15 @@ OBJECTS_ENDPOINT = "/api/v1/admin/scrape/kn-catalog-objects"
FLATS_ENDPOINT = "/api/v1/admin/scrape/kn-catalog-flats"
def _assert_refusal_names_real_condition(detail: str) -> None:
assert "2443" in detail
assert "i_understand_waf_risk" in detail
# Условие снятия — прокси и принятый kn-прогон (#3307). Обещание «cooldown
# прошёл» подсказывало оператору обойти блокировку, которую ожидание не снимает.
assert "3307" in detail
assert "cooldown" not in detail.lower()
def _mock_apply_async_result(task_id: str = "fake-task-id") -> MagicMock:
result = MagicMock()
result.id = task_id
@ -33,9 +43,7 @@ def test_kn_catalog_objects_refuses_without_override() -> None:
resp = TestClient(app).post(OBJECTS_ENDPOINT, json={"region_code": 66})
assert resp.status_code == 400
detail = resp.json()["detail"]
assert "2443" in detail
assert "i_understand_waf_risk" in detail
_assert_refusal_names_real_condition(resp.json()["detail"])
mock_apply_async.assert_not_called()
@ -68,9 +76,7 @@ def test_kn_catalog_flats_refuses_without_override() -> None:
resp = TestClient(app).post(FLATS_ENDPOINT, json={"region_code": 66})
assert resp.status_code == 400
detail = resp.json()["detail"]
assert "2443" in detail
assert "i_understand_waf_risk" in detail
_assert_refusal_names_real_condition(resp.json()["detail"])
mock_apply_async.assert_not_called()

View file

@ -1919,17 +1919,17 @@ export interface paths {
* @description Manual trigger для catalog-OBJECT scraper (заполняет wall_type, energy_eff,
* ceiling_height_m, parking_*, playground_*, scores из SSR __NEXT_DATA__).
*
* Beat schedule: Tuesday 04:00 UTC, batch 300/run. Этот endpoint для ad-hoc
* запуска (smoke-тест после деплоя или повторный pass для свежесозданных
* объектов до next beat fire).
* Beat-запись выключена (блокировка StormWall, #2443; см. beat_schedule.py).
* Этот endpoint для ad-hoc запуска (smoke-тест или повторный pass для
* свежесозданных объектов).
*
* - max_objects=None дефолтный лимит таска (300).
* - max_objects=3 smoke-тест.
* - force=True "Загрузить все": игнорирует skip-today, грузит всё подряд.
*
* WAF cooldown guard (#2443, #2445 D1): требует i_understand_waf_risk=true
* beat schedule для этого таска отключён из-за WAF hard-ban 2026-05-24, ad-hoc
* re-trigger без явного подтверждения оператора запрещён.
* Guard блокировки DOM.РФ (#2443, #2445 D1): требует i_understand_waf_risk=true
* beat schedule для этого таска выключен из-за блокировки StormWall, ad-hoc
* запуск без явного подтверждения оператора запрещён (условие #3307).
*/
post: operations["trigger_kn_catalog_objects_api_v1_admin_scrape_kn_catalog_objects_post"];
delete?: never;
@ -1960,9 +1960,9 @@ export interface paths {
* - max_flats=3 smoke-тест.
* - force=True 'Загрузить все': игнорирует фильтр свежести, грузит всё с hash.
*
* WAF cooldown guard (#2443, #2445 D1): требует i_understand_waf_risk=true
* same /сервисы/* BrowserSession path family как catalog-objects, риск re-trigger
* того же WAF-бана.
* Guard блокировки DOM.РФ (#2443, #2445 D1): требует i_understand_waf_risk=true
* same /сервисы/* BrowserSession path family как catalog-objects, закрыт той же
* блокировкой StormWall (условие #3307).
*/
post: operations["trigger_kn_catalog_flats_api_v1_admin_scrape_kn_catalog_flats_post"];
delete?: never;