fix(tradein/domclick): мы считали блоком собственное недоделанное рукопожатие #3237

Merged
bot-backend merged 1 commit from fix/domclick-challenge-not-a-block into main 2026-08-29 15:38:06 +00:00
Owner

Корневая причина двухнедельного attempted=3, blocked=3, enriched=0 у domclick_detail_backfill. Мы не натыкались на защиту площадки — мы прерывали собственный обмен с ней и наказывали за это узел.

Механизм

Камуфокс решает QRATOR-PoW сам. Но сайдкар отдаёт HTML после слепой паузы BROWSER_WAIT_MS, и если PoW ещё не досчитан, наверх уезжает недоделанная страница. Парсер не находит __SSR_STATE__, ловит DOMCLICK_BLOCK_MARKERS и поднимает DomClickBlockedError — сообщение внутри которого буквально гласит "challenge page detected".

Дальше по цепочке:

  1. blocked++, consecutive_blocks++
  2. report_ban — здоровый узел уходит в бан на двое суток за то, что показал челлендж
  3. срабатывает единственный за прогон request_context_reset()выбрасывается контекст, которому оставался один цикл опроса до рабочего пропуска
  4. следующая карточка: чистый контекст → снова рукопожатие → «блок» №2 → №3 → ABORT

Ветка ожидания _wait_out_pow_challenge при этом существует и исправна — её написали для Авито в #3045. Она просто не включалась для Домклика: _CHALLENGE_MARKERS (startpow, «Доступ ограничен: проверка безопасности») сняты с Авито, и на страницах Домклика их нет. Код это даже признаёт в комментарии на server.py:1184.

Замеры прода 29.08

Здоровый узел, один контекст, одна и та же карточка трижды подряд:

запрос HTTP размер итог
1 401 6 898 ни __SSR_STATE__, ни маркеров
2 200 877 КБ __SSR_STATE__
3 200 872 КБ __SSR_STATE__

Пропуск зарабатывается на первом запросе. Достаточно было его дождаться.

Различимые состояния (маркеры сняты вживую, как предписывает комментарий на :1161):

состояние признак размер HTTP
загрузчик рукопожатия <script src="/__qrator/qauth_*.js"> 279 401
рендеренная PoW-страница ни одного стабильного маркера ~6 898–7 086 401
отказ площадки <title>403 | Домклик</title>, «Похоже, ваш запрос выглядит необычно» 26 625–26 626 401
успех __SSR_STATE__ 540–910 КБ 200

Решение: инвертировать опознание

Опознавать челлендж положительно нельзя — у рендеренной PoW-страницы стабильных строк нет вовсе, любой такой маркер протухнет на следующей смене вёрстки. Поэтому для provider == "domclick" положительно опознаются только два крайних состояния:

  • __SSR_STATE__ → успех, отдаём сразу;
  • маркеры отказа → настоящий блок, BanPageDetectedError, ждать нечего;
  • всё остальное → рукопожатие ещё идёт → существующий _wait_out_pow_challenge, без бана и без сброса контекста.

_wait_out_pow_challenge получил опциональный is_pending (дефолт — прежний _is_pow_challenge), поэтому Авито, Циан, Яндекс и generic идут по старой ветке байт-в-байт.

_REFUSAL_STATUSES намеренно не тронут. Добавить туда 401 — заманчиво и неверно: с этим кодом сегодня приходят и челлендж, и отказ, и успешная страница на 842 КБ. Для Домклика статус не смотрится вовсе, решает только тело.

Известный компромисс

Если Домклик переименует __SSR_STATE__ (дрейф схемы), карточка будет ждать полные BROWSER_CHALLENGE_WAIT_MS = 30 с вместо быстрой ошибки разбора, и прогон прервётся после трёх таких. Узлы при этом не банятся: ChallengeTimeoutError уходит наверх и providers/domclick/detail.py:547-555 заворачивает его без mark_banned. Считаю размен приемлемым — цена дрейфа втрое меньше, чем цена нынешнего ложного бана.

Тесты

Новый tradein-mvp/browser/test_server_domclick_challenge.py — детекторы плюс 7 сценариев _fetch_once: отказ сразу, ожидание без маркеров → успех, успех сразу, отказ уже во время ожидания, исчерпание бюджета, 401 не трактуется как отказ, не-домклик провайдер не задет.

test_server_reuse_context.py — два существующих теста: их fake-страница отдавала generic HTML при provider="domclick" и под новой логикой корректно уходила в ожидание. В фикстуру добавлен __SSR_STATE__. Проверено отдельно, что это адаптация, а не маскировка: страница действительно прогоняется через новую ветку, без маркера тест падал бы по существу.

Полный набор сайдкара: 174 passed. Ревью — APPROVE, критичных замечаний нет.

Приёмка после деплоя

Плановый domclick_detail_backfill. В counters ждём blocked заметно меньше attempted и ненулевой enriched — сейчас там attempted=3, blocked=3, enriched=0. В логах сайдкара должно появиться «PoW-челлендж снят за ~Nмс» для домклика, чего раньше не было никогда.

Оговорка: узлы пула 1/9/10/11 сегодня подпорчены диагностическими пробами, поэтому первый прогон может упереться в остаточные отказы площадки, а не в эту правку. Судить по второму.

Refs #3190, #2854, #3212, #3196, #3045.

Корневая причина двухнедельного `attempted=3, blocked=3, enriched=0` у `domclick_detail_backfill`. Мы не натыкались на защиту площадки — мы прерывали собственный обмен с ней и наказывали за это узел. ## Механизм Камуфокс решает QRATOR-PoW сам. Но сайдкар отдаёт HTML после **слепой** паузы `BROWSER_WAIT_MS`, и если PoW ещё не досчитан, наверх уезжает недоделанная страница. Парсер не находит `__SSR_STATE__`, ловит `DOMCLICK_BLOCK_MARKERS` и поднимает `DomClickBlockedError` — сообщение внутри которого буквально гласит `"challenge page detected"`. Дальше по цепочке: 1. `blocked++`, `consecutive_blocks++` 2. `report_ban` — здоровый узел уходит в бан на двое суток за то, что показал челлендж 3. срабатывает единственный за прогон `request_context_reset()` — **выбрасывается контекст, которому оставался один цикл опроса до рабочего пропуска** 4. следующая карточка: чистый контекст → снова рукопожатие → «блок» №2 → №3 → `ABORT` Ветка ожидания `_wait_out_pow_challenge` при этом **существует и исправна** — её написали для Авито в #3045. Она просто не включалась для Домклика: `_CHALLENGE_MARKERS` (`startpow`, «Доступ ограничен: проверка безопасности») сняты с Авито, и на страницах Домклика их нет. Код это даже признаёт в комментарии на `server.py:1184`. ## Замеры прода 29.08 Здоровый узел, один контекст, одна и та же карточка трижды подряд: | запрос | HTTP | размер | итог | |---|---|---|---| | 1 | 401 | 6 898 | ни `__SSR_STATE__`, ни маркеров | | 2 | **200** | 877 КБ | **`__SSR_STATE__`** | | 3 | **200** | 872 КБ | **`__SSR_STATE__`** | Пропуск зарабатывается на первом запросе. Достаточно было его дождаться. Различимые состояния (маркеры сняты вживую, как предписывает комментарий на `:1161`): | состояние | признак | размер | HTTP | |---|---|---|---| | загрузчик рукопожатия | `<script src="/__qrator/qauth_*.js">` | 279 | 401 | | рендеренная PoW-страница | **ни одного** стабильного маркера | ~6 898–7 086 | 401 | | отказ площадки | `<title>403 \| Домклик</title>`, «Похоже, ваш запрос выглядит необычно» | 26 625–26 626 | **401** | | успех | `__SSR_STATE__` | 540–910 КБ | 200 | ## Решение: инвертировать опознание Опознавать челлендж положительно нельзя — у рендеренной PoW-страницы стабильных строк нет вовсе, любой такой маркер протухнет на следующей смене вёрстки. Поэтому для `provider == "domclick"` положительно опознаются только два крайних состояния: - `__SSR_STATE__` → успех, отдаём сразу; - маркеры отказа → настоящий блок, `BanPageDetectedError`, ждать нечего; - **всё остальное** → рукопожатие ещё идёт → существующий `_wait_out_pow_challenge`, без бана и без сброса контекста. `_wait_out_pow_challenge` получил опциональный `is_pending` (дефолт — прежний `_is_pow_challenge`), поэтому Авито, Циан, Яндекс и generic идут по старой ветке байт-в-байт. **`_REFUSAL_STATUSES` намеренно не тронут.** Добавить туда 401 — заманчиво и неверно: с этим кодом сегодня приходят и челлендж, и отказ, и успешная страница на 842 КБ. Для Домклика статус не смотрится вовсе, решает только тело. ## Известный компромисс Если Домклик переименует `__SSR_STATE__` (дрейф схемы), карточка будет ждать полные `BROWSER_CHALLENGE_WAIT_MS` = 30 с вместо быстрой ошибки разбора, и прогон прервётся после трёх таких. Узлы при этом **не банятся**: `ChallengeTimeoutError` уходит наверх и `providers/domclick/detail.py:547-555` заворачивает его без `mark_banned`. Считаю размен приемлемым — цена дрейфа втрое меньше, чем цена нынешнего ложного бана. ## Тесты Новый `tradein-mvp/browser/test_server_domclick_challenge.py` — детекторы плюс 7 сценариев `_fetch_once`: отказ сразу, ожидание без маркеров → успех, успех сразу, отказ уже во время ожидания, исчерпание бюджета, 401 не трактуется как отказ, не-домклик провайдер не задет. `test_server_reuse_context.py` — два существующих теста: их fake-страница отдавала generic HTML при `provider="domclick"` и под новой логикой корректно уходила в ожидание. В фикстуру добавлен `__SSR_STATE__`. Проверено отдельно, что это адаптация, а не маскировка: страница действительно прогоняется через новую ветку, без маркера тест падал бы по существу. Полный набор сайдкара: **174 passed**. Ревью — ✅ APPROVE, критичных замечаний нет. ## Приёмка после деплоя Плановый `domclick_detail_backfill`. В `counters` ждём `blocked` заметно меньше `attempted` и ненулевой `enriched` — сейчас там `attempted=3, blocked=3, enriched=0`. В логах сайдкара должно появиться «PoW-челлендж снят за ~Nмс» для домклика, чего раньше не было никогда. Оговорка: узлы пула 1/9/10/11 сегодня подпорчены диагностическими пробами, поэтому первый прогон может упереться в остаточные отказы площадки, а не в эту правку. Судить по второму. Refs #3190, #2854, #3212, #3196, #3045.
lekss361 added 1 commit 2026-08-29 15:36:11 +00:00
fix(tradein/domclick): недосчитанная QRATOR-страница уходила наверх как контент
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 12s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 1m15s
5391a36880
Домклик отдаёт рукопожатие без единого стабильного маркера (в отличие от
Авито), поэтому _CHALLENGE_MARKERS (сняты с Авито, #3045) на нём никогда не
матчились и ветка ожидания не включалась — недосчитанная страница уезжала
наверх, парсер не находил __SSR_STATE__ и поднимал ложный блок. Это и был
двухнедельный attempted=3, blocked=3, enriched=0 у domclick_detail_backfill.

Для provider=="domclick" логика инвертирована: положительно опознаём только
два крайних состояния — успех (__SSR_STATE__) и статический отказ площадки
(«403 | Домклик» / «похоже, ваш запрос выглядит необычно»); всё остальное
(загрузчик рукопожатия, нерендеренная PoW-страница без каких-либо маркеров)
трактуется как «рукопожатие ещё идёт» и уходит в существующий
_wait_out_pow_challenge с кастомным is_pending. HTTP-статус для DomClick не
используется как сигнал (401 приходит и у отказа, и у успеха, и у здорового
рукопожатия) — решает только тело. Avito и прочие провайдеры идут по старой
elif-ветке без изменений.

_wait_out_pow_challenge получил опциональный параметр is_pending (дефолт
_is_pow_challenge) — golden-parity для всех, кроме domclick.
bot-backend merged commit d43d8b0c60 into main 2026-08-29 15:38:06 +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#3237
No description provided.