fix(ptica): серия WAF-блоков прерывает батч каталога, а не гонит его до конца (#2464) #2971

Merged
bot-backend merged 1 commit from fix/2464-catalog-waf-breaker into main 2026-08-20 10:54:12 +00:00
Collaborator

Пункт эпика #2464: domrf_catalog_object.py:460. Та же семья, что #2969.

Дефект

except WafBlockedError as exc:
    logger.warning("catalog_object WAF blocked obj_id=%d: %s", obj_id, exc)
    return False

WAF-блок возвращался как обычная неудача, поэтому батч-цикл шёл дальше и слал живой запрос на каждый оставшийся obj_id в уже забаненную сессию.

Цена

Замер прода 20.08: в очереди 13 200 объектов из 13 801, а DOM.РФ отдаёт страницу «Доступ заблокирован [403]» с капчей (#2443).

Один прогон «Загрузить все» выдал бы 13 200 живых запросов в забаненную сессию — ровно то, что углубляет бан. Анти-бан-комментарий к BrowserSession в этом же файле предупреждает про тот самый path family, из-за которого бан и случился.

Правка

  1. Per-object функция пробрасывает WAF — это не «объект не дошёл», а закрытая дверь.
  2. Батч-цикл считает подряд идущие блоки и прерывается на третьем.

Порог не единица намеренно: одиночный блок бывает переходным, три подряд — стена. Прерванный батч помечается aborted_on_waf в stats, а stats уходит в результат задачи через **stats — видно в логе прогона.

Про двусторонность — честно

Части проверяются по-разному, и в шапке теста это написано:

  • Проброс WAF — двусторонне. Тест подменяет fetch_catalog_object_html (настоящий источник блока) и гоняет настоящую функцию: на origin/mainDID NOT RAISE, потому что там исключение гасится.
  • Предохранитель — логики этой на origin/main нет, поэтому тесты подменяют scrape_catalog_object и меряют новый цикл в изоляции: сколько объектов он успел тронуть. На двустороннюю проверку они не претендуют.

Первая версия теста этого не различала: подменяла scrape_catalog_object и на main падала с пробросом исключения — то есть по «нет такого кода», а не по неверному поведению. Перестроено.

Контроли

проверка что ловит
одиночный блок не рвёт прогон «починку» через abort-on-first — переходный блок стоил бы всей докачки
успех между блоками сбрасывает счётчик счёт не-подряд идущих блоков
обычный сбой сети по-прежнему False проброс вообще всего — один битый объект рвал бы батч
здоровый батч проходит все объекты лишнее обрезание

Прогоны

tests/services/scrapers + tests/workers   525 passed   rc=0
Пункт эпика #2464: `domrf_catalog_object.py:460`. Та же семья, что #2969. ## Дефект ```python except WafBlockedError as exc: logger.warning("catalog_object WAF blocked obj_id=%d: %s", obj_id, exc) return False ``` WAF-блок возвращался как обычная неудача, поэтому батч-цикл шёл дальше и слал **живой запрос на каждый оставшийся** `obj_id` в уже забаненную сессию. ## Цена Замер прода 20.08: в очереди **13 200 объектов из 13 801**, а DOM.РФ отдаёт страницу «Доступ заблокирован [403]» с капчей (#2443). Один прогон «Загрузить все» выдал бы 13 200 живых запросов в забаненную сессию — ровно то, что углубляет бан. Анти-бан-комментарий к `BrowserSession` **в этом же файле** предупреждает про тот самый path family, из-за которого бан и случился. ## Правка 1. Per-object функция **пробрасывает** WAF — это не «объект не дошёл», а закрытая дверь. 2. Батч-цикл считает **подряд идущие** блоки и прерывается на третьем. Порог не единица намеренно: одиночный блок бывает переходным, три подряд — стена. Прерванный батч помечается `aborted_on_waf` в stats, а stats уходит в результат задачи через `**stats` — видно в логе прогона. ## Про двусторонность — честно Части проверяются по-разному, и в шапке теста это написано: - **Проброс WAF — двусторонне.** Тест подменяет `fetch_catalog_object_html` (настоящий источник блока) и гоняет **настоящую** функцию: на `origin/main` — `DID NOT RAISE`, потому что там исключение гасится. - **Предохранитель** — логики этой на `origin/main` нет, поэтому тесты подменяют `scrape_catalog_object` и меряют новый цикл в изоляции: сколько объектов он успел тронуть. На двустороннюю проверку они не претендуют. Первая версия теста этого не различала: подменяла `scrape_catalog_object` и на main падала с пробросом исключения — то есть по «нет такого кода», а не по неверному поведению. Перестроено. ## Контроли | проверка | что ловит | |---|---| | одиночный блок не рвёт прогон | «починку» через abort-on-first — переходный блок стоил бы всей докачки | | успех между блоками сбрасывает счётчик | счёт не-подряд идущих блоков | | обычный сбой сети по-прежнему `False` | проброс вообще всего — один битый объект рвал бы батч | | здоровый батч проходит все объекты | лишнее обрезание | ## Прогоны ``` tests/services/scrapers + tests/workers 525 passed rc=0 ```
bot-backend added 1 commit 2026-08-20 10:34:05 +00:00
fix(ptica): серия WAF-блоков прерывает батч каталога, а не гонит его до конца (#2464)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m16s
CI / backend-tests (pull_request) Successful in 17m10s
cb5e2d4f70
scrape_catalog_object гасил WafBlockedError и возвращал его как обычную неудачу,
поэтому батч-цикл шёл дальше и слал ЖИВОЙ запрос на каждый оставшийся obj_id в
уже забаненную сессию.

Цена. Замер прода 20.08: в очереди 13200 объектов из 13801, а DOM.РФ отдаёт
страницу «Доступ заблокирован [403]» с капчей (#2443). Один прогон «Загрузить
все» выдал бы 13200 живых запросов в забаненную сессию — ровно то, что углубляет
бан. Анти-бан-комментарий к BrowserSession в этом же файле предупреждает про тот
же path family, из-за которого бан и случился.

Правка из двух частей: per-object функция пробрасывает WAF (это не «объект не
дошёл», а закрытая дверь), батч-цикл считает ПОДРЯД идущие блоки и прерывается на
третьем. Порог не единица намеренно: одиночный блок бывает переходным, три
подряд — стена. Прерванный батч помечается в stats (aborted_on_waf), а stats
уходит в результат задачи через **stats — видно в логе прогона.

Про двусторонность честно: части проверяются по-разному.

  • Проброс WAF — ДВУСТОРОННЕ. Тест подменяет fetch_catalog_object_html
    (настоящий источник блока) и гоняет НАСТОЯЩУЮ функцию: на origin/main
    «DID NOT RAISE», потому что там исключение гасится.
  • Предохранитель — логики этой на origin/main нет, поэтому тесты подменяют
    scrape_catalog_object и меряют новый цикл в изоляции (сколько объектов он
    успел тронуть). На двустороннюю проверку они не претендуют, и это написано
    в шапке файла.

Первая версия теста этого не различала: подменяла scrape_catalog_object и на main
падала с пробросом исключения — то есть по «нет такого кода», а не по неверному
поведению. Перестроено.

Контроли: одиночный блок не рвёт прогон; успех между блоками сбрасывает счётчик;
обычный сбой сети по-прежнему гасится в False (иначе один битый объект рвал бы
батч); здоровый батч проходит все объекты.

Прогоны: tests/services/scrapers + tests/workers — 525 passed rc=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 6e34928dd4 into main 2026-08-20 10:54:12 +00:00
Sign in to join this conversation.
No reviewers
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#2971
No description provided.