From 663f426d4c9a78b0001703bda5c7d46780f0f3cf Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 29 Aug 2026 12:19:18 +0300 Subject: [PATCH] =?UTF-8?q?fix(tradein/domclick):=20=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=20=D1=81=20=D0=BA=D0=BE=D0=B4=D0=BE=D0=BC=20401=20?= =?UTF-8?q?=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D0=B8=D1=84=D0=B8=D1=86=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BB=D1=81=D1=8F=20=D0=BA=D0=B0=D0=BA?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D1=87=D0=B8=D0=BD=D0=B0=20=D0=BD=D0=B5?= =?UTF-8?q?=D0=B8=D0=B7=D0=B2=D0=B5=D1=81=D1=82=D0=BD=D0=BE=D0=B9=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D1=80=D0=BE=D0=B4=D1=8B=20(#3196)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Замер на проде 2026-08-29: страница отказа ДомКлика при HTTP 401 — РОВНО 26 624 байта, байт в байт та же, что снималась 28.08 под кодом 403. Ни PoW, ни QRATOR, ни капчи. Приходит одинаково и с валидной сохранённой сессией (16 куков из domclick_session), и полностью анонимно — значит это не «сессия отвергнута», а WAF-отказ с подменённым кодом ответа. В таблице классификатора 401 не было (403/429 → platform, 5xx → infra), поэтому все три пробы подряд дали ban_kind='unknown' — ровно то, что #3196 и должен был убрать. Механика диагноза при этом рабочая: статус доезжает от page.goto до исключения целым, шов blocked.status = status подтверждён живым 401 на проде. Правка доменная — в _ban_kind_of_block домкликового таска, а не в общей scraper_kit.browser_fetcher.ban_kind_from_status: у других поставщиков 401 обычно значит «наша сессия протухла», это наша сторона, и метка 'platform' там зря запустила бы ротацию IP (#2611). --- .../app/tasks/domclick_detail_backfill.py | 23 +++++++++++++++---- .../tests/test_3196_domclick_ban_kind.py | 1 + 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/tradein-mvp/backend/app/tasks/domclick_detail_backfill.py b/tradein-mvp/backend/app/tasks/domclick_detail_backfill.py index 84a8f85a..a81ea102 100644 --- a/tradein-mvp/backend/app/tasks/domclick_detail_backfill.py +++ b/tradein-mvp/backend/app/tasks/domclick_detail_backfill.py @@ -101,7 +101,7 @@ from app.core.config import settings from app.core.shutdown import shutdown_requested from app.services import domclick_session as domclick_session_svc from app.services import scrape_runs as runs_mod -from app.services.scrape_runs import BAN_KIND_UNKNOWN +from app.services.scrape_runs import BAN_KIND_PLATFORM, BAN_KIND_UNKNOWN logger = logging.getLogger(__name__) @@ -164,17 +164,32 @@ def _warn_before_domclick_cookies_expire(db: Session, run_id: int) -> None: ) +# 401 у ДомКлика — тот же отказ площадки, что 403 (замер прод 2026-08-29): страница отказа +# при 401 РОВНО 26 624 байта, байт в байт совпадает со страницей `403 | Домклик` +# (снята 28.08.2026), без PoW/QRATOR/капчи. Приходит одинаково и с валидной сохранённой +# сессией (16 куков domclick_session), и полностью анонимно — значит это не «сессия +# отвергнута» (тогда анонимный запрос вёл бы себя иначе), а WAF-отказ с подменённым кодом. +# Правка ДОМЕННАЯ, не в общей ban_kind_from_status (scraper_kit.browser_fetcher): у других +# поставщиков 401 обычно значит «наша сессия протухла» (наша сторона) — общая метка +# 'platform' там зря запустила бы ротацию IP (#2611). +_DOMCLICK_REFUSAL_STATUSES = frozenset({401}) + + def _ban_kind_of_block(exc: DomClickBlockedError) -> str: - """Диагноз одного блока по HTTP-статусу ответа площадки (#3196). + """Диагноз одного блока по HTTP-статусу ответа площадки (#3196, #3178). Форма повторяет avito_detail_backfill (ban_kind_of_exception → Counter → mark_backfill_finished(ban_kinds=...)), но опора другая: у Авито причину несёт ТИП исключения (AvitoSidecarUnavailableError vs AvitoBlockedError), у Домклика - тип один на оба случая, поэтому смотрим статус. 403/429 → 'platform', 5xx → + тип один на оба случая, поэтому смотрим статус. 401/403/429 → 'platform' (401 — + доменное исключение из общего правила, см. _DOMCLICK_REFUSAL_STATUSES), 5xx → 'infra' (ban_kind_from_status), статуса нет → 'unknown' — не назначаем причину, которую не установили (#2764). """ - return ban_kind_from_status(getattr(exc, "status", None)) or BAN_KIND_UNKNOWN + status = getattr(exc, "status", None) + if status in _DOMCLICK_REFUSAL_STATUSES: + return BAN_KIND_PLATFORM + return ban_kind_from_status(status) or BAN_KIND_UNKNOWN @dataclass diff --git a/tradein-mvp/backend/tests/test_3196_domclick_ban_kind.py b/tradein-mvp/backend/tests/test_3196_domclick_ban_kind.py index 9cf520d8..7d28425c 100644 --- a/tradein-mvp/backend/tests/test_3196_domclick_ban_kind.py +++ b/tradein-mvp/backend/tests/test_3196_domclick_ban_kind.py @@ -119,6 +119,7 @@ def _ban_kinds_of(runs: MagicMock) -> Counter: @pytest.mark.parametrize( ("status", "expected"), [ + (401, "platform"), # прод 28-29.08: страница отказа байт-в-байт как у 403 (403, "platform"), # ровно тот случай, что намеряли вживую: `403 | Домклик` (429, "platform"), (500, "infra"),