Домклик detail: сайдкар даёт чистый контекст на каждый запрос, мы реплеим протухший qrator_jsid2 — отсюда «одна карточка и стена» #3190

Closed
opened 2026-08-28 18:23:49 +00:00 by lekss361 · 3 comments
Owner

Корневая причина двухнедельного attempted=4, enriched=1, blocked=3 у domclick_detail_backfill. Найдено и подтверждено живыми замерами на проде 28.08.

Механизм

  1. browser/server.py, _fetch_once (~строка 1085): на КАЖДЫЙ /fetch делается page = await browser.new_page(). _browsers[provider] — это Browser от AsyncCamoufox, значит каждый запрос получает новый изолированный контекст, и cookie-jar умирает сразу после ответа.
  2. Туда же вливается замороженный снимок кук из domclick_session_cookies (add_cookies, там же). Живой qrator_jsid2, который площадка отдаёт через Set-Cookie, выбрасывается вместе с контекстом.
  3. qrator_jsid2 живёт ~2.5 часа и ротируется площадкой (у свежей сессии срок стоял на +2ч20м), а у нас хранится 30 дней (save_session, ttl_days=30).

Итог: первый запрос после заливки кук проходит с ещё живым токеном, все следующие показывают QRATOR один и тот же протухший — он отдаёт челлендж-страницу без __SSR_STATE__, и это ловится как DomClickBlockedError.

Замеры (прод, 28.08)

Свежий аккаунт залит, сессия валидна, куки доезжают до боевого пути (last_used_at совпадает с моментом старта прогона).

Условие Результат
первая карточка после заливки кук OKitem_id, year_built=1981, 2 записи истории цены
12 карточек подряд, тот же путь 12 блоков из 12
6 карточек, аренда из пула (узлы 1 → 9) 6 из 6
8 карточек, узел 10 со свежим exit-IP после ротации 8 из 8, блок с первого запроса
те же 5 карточек в тёплом контексте с эволюционирующими куками 5 из 5 успешно, __SSR_STATE__ есть, ~2 сек каждая

Свежий exit-IP блокируется мгновенно — значит дело не в репутации адреса. Тёплый контекст на тех же карточках проходит полностью — значит дело в замороженных куках.

Правка

Opt-in тёплый контекст для ДомКлика: сайдкар держит переиспользуемый browser.new_context() на провайдера, куки вливаются один раз при создании, дальше jar живёт сам; после блока контекст сбрасывается. Для остальных провайдеров поведение не меняется (флаг по умолчанию выключен).

Ветка fix/3118-domclick-warm-context.

Смежные находки (отдельными тикетами)

  • domclick_detail_backfill.py:286 поднимает BrowserFetcher без провайдера пула — use_pool=False, proxy_provider=None. Пул из 4 узлов не задействован вовсе, браузер идёт через единственный статический SCRAPER_PROXY_URL. Тот же класс, что чинили для Авито в #3045.
  • Челлендж приходит валидным HTTP-ответом → _report_fetch_result(True), счётчик провалов обнуляется, узел не ротируется по факту блока; report_ban() бэкфилл не зовёт.
  • DomClickBlockedError смешивает anti-bot маркер и любой сбой фетча; ban_kind не передаётся → в прогонах всегда unknown.
  • load_session = ORDER BY uploaded_at DESC LIMIT 1: чередования аккаунтов нет, новая заливка навсегда вытесняет предыдущую и сбрасывает last_invalid_at.
  • Кука TGC (Sber ID, годна 3 месяца, path=/cas/) в белый список не входит и не сохраняется — возможно, позволяет перевыпускать сессию без SMS.
  • qrator_jsid2 хранится 30 дней при реальном сроке жизни в часы — в снимке она бесполезна независимо от прочего.

Refs #3118, #3171

Корневая причина двухнедельного `attempted=4, enriched=1, blocked=3` у `domclick_detail_backfill`. Найдено и подтверждено живыми замерами на проде 28.08. ## Механизм 1. `browser/server.py`, `_fetch_once` (~строка 1085): на КАЖДЫЙ `/fetch` делается `page = await browser.new_page()`. `_browsers[provider]` — это Browser от `AsyncCamoufox`, значит каждый запрос получает **новый изолированный контекст**, и cookie-jar умирает сразу после ответа. 2. Туда же вливается замороженный снимок кук из `domclick_session_cookies` (`add_cookies`, там же). Живой `qrator_jsid2`, который площадка отдаёт через `Set-Cookie`, выбрасывается вместе с контекстом. 3. `qrator_jsid2` живёт **~2.5 часа** и ротируется площадкой (у свежей сессии срок стоял на +2ч20м), а у нас хранится **30 дней** (`save_session`, `ttl_days=30`). Итог: первый запрос после заливки кук проходит с ещё живым токеном, все следующие показывают QRATOR один и тот же протухший — он отдаёт челлендж-страницу без `__SSR_STATE__`, и это ловится как `DomClickBlockedError`. ## Замеры (прод, 28.08) Свежий аккаунт залит, сессия валидна, куки доезжают до боевого пути (`last_used_at` совпадает с моментом старта прогона). | Условие | Результат | |---|---| | первая карточка после заливки кук | **OK** — `item_id`, `year_built=1981`, 2 записи истории цены | | 12 карточек подряд, тот же путь | 12 блоков из 12 | | 6 карточек, аренда из пула (узлы 1 → 9) | 6 из 6 | | 8 карточек, узел 10 **со свежим exit-IP после ротации** | 8 из 8, блок с первого запроса | | **те же 5 карточек в тёплом контексте с эволюционирующими куками** | **5 из 5 успешно**, `__SSR_STATE__` есть, ~2 сек каждая | Свежий exit-IP блокируется мгновенно — значит дело не в репутации адреса. Тёплый контекст на тех же карточках проходит полностью — значит дело в замороженных куках. ## Правка Opt-in тёплый контекст для ДомКлика: сайдкар держит переиспользуемый `browser.new_context()` на провайдера, куки вливаются **один раз при создании**, дальше jar живёт сам; после блока контекст сбрасывается. Для остальных провайдеров поведение не меняется (флаг по умолчанию выключен). Ветка `fix/3118-domclick-warm-context`. ## Смежные находки (отдельными тикетами) - `domclick_detail_backfill.py:286` поднимает `BrowserFetcher` без провайдера пула — `use_pool=False`, `proxy_provider=None`. Пул из 4 узлов не задействован вовсе, браузер идёт через единственный статический `SCRAPER_PROXY_URL`. Тот же класс, что чинили для Авито в #3045. - Челлендж приходит валидным HTTP-ответом → `_report_fetch_result(True)`, счётчик провалов обнуляется, узел не ротируется по факту блока; `report_ban()` бэкфилл не зовёт. - `DomClickBlockedError` смешивает anti-bot маркер и любой сбой фетча; `ban_kind` не передаётся → в прогонах всегда `unknown`. - `load_session` = `ORDER BY uploaded_at DESC LIMIT 1`: чередования аккаунтов нет, новая заливка навсегда вытесняет предыдущую и сбрасывает `last_invalid_at`. - Кука `TGC` (Sber ID, годна 3 месяца, `path=/cas/`) в белый список не входит и не сохраняется — возможно, позволяет перевыпускать сессию без SMS. - `qrator_jsid2` хранится 30 дней при реальном сроке жизни в часы — в снимке она бесполезна независимо от прочего. Refs #3118, #3171
lekss361 added the
bug
priority/p1
scope/backend
scrapers
tradein
labels 2026-08-28 18:24:09 +00:00
Author
Owner

Приёмка после деплоя: фикс не сработал. Гипотеза была неверной

PR #3193 смержен, деплой доехал, код в контейнере проверен (grep reuse_context в tradein-browser → 19 вхождений, сайдкар новый). Замер тем же способом, что ловил баг: 10 карточек из очереди, боевой путь, reuse_context=True.

Результат: 1 успех, 9 блоков. Ровно та же картина, что до правки.

Замер До правки После
карточек взято 1 из 12 1 из 10
дальше стена стена

Тёплый контекст теперь действительно живёт между запросами, но карточка №2 уже блокируется — при живом, непересозданном контексте. Значит замороженный qrator_jsid2 был следствием, а не причиной: он объяснял наблюдения, но его починка ничего не изменила.

Что осталось необъяснённым и что это на самом деле

Развилка сузилась до одного отличия. Одни и те же карточки:

  • настоящий Chrome, домашний канал, тёплый контекст → 5 из 5;
  • camoufox через прокси, тёплый контекст → 1 из 10.

При этом свежеротированный exit-IP блокировался с первого запроса, то есть репутация адреса тоже ни при чём.

Остаётся то, что в настоящем браузере JS-челлендж QRATOR исполняется и токен обновляется сам, а camoufox его не проходит — страница возвращается без __SSR_STATE__. Первый запрос проскакивает на ещё живом токене из снимка кук, всё последующее упирается в непройденный челлендж.

Это ровно #3045 — «QRATOR отдаёт proof-of-work челлендж, а мы его не проходим ни одним из путей». Тот же вендор защиты, тот же отказ, только там он вскрылся на Авито. Корень общий для двух поставщиков, и решать его надо там, а не в домкликовской частности.

Что делать с уже влитым

Откатывать #3193 не предлагаю: правка opt-in, для остальных трёх поставщиков payload не меняется, тесты зелёные, а тёплый контекст сам по себе корректнее прежнего поведения и понадобится, когда челлендж научимся проходить. Но она не закрывает эту задачу — снимаю с неё эту роль.

Важное следствие для #3197: ключ контекста по провайдеру безопасен ровно до тех пор, пока ДомКлик ходит мимо пула. Подключение пула без смены ключа даст новую поломку.

Задача остаётся открытой, корневая причина переезжает в #3045.

## Приёмка после деплоя: фикс не сработал. Гипотеза была неверной PR #3193 смержен, деплой доехал, код в контейнере проверен (`grep reuse_context` в `tradein-browser` → 19 вхождений, сайдкар новый). Замер тем же способом, что ловил баг: 10 карточек из очереди, боевой путь, `reuse_context=True`. **Результат: 1 успех, 9 блоков.** Ровно та же картина, что до правки. | Замер | До правки | После | |---|---|---| | карточек взято | 1 из 12 | **1 из 10** | | дальше | стена | **стена** | Тёплый контекст теперь действительно живёт между запросами, но карточка №2 уже блокируется — при живом, непересозданном контексте. Значит замороженный `qrator_jsid2` был **следствием, а не причиной**: он объяснял наблюдения, но его починка ничего не изменила. ## Что осталось необъяснённым и что это на самом деле Развилка сузилась до одного отличия. Одни и те же карточки: - настоящий Chrome, домашний канал, тёплый контекст → **5 из 5**; - camoufox через прокси, тёплый контекст → **1 из 10**. При этом свежеротированный exit-IP блокировался с первого запроса, то есть репутация адреса тоже ни при чём. Остаётся то, что в настоящем браузере **JS-челлендж QRATOR исполняется и токен обновляется сам**, а camoufox его не проходит — страница возвращается без `__SSR_STATE__`. Первый запрос проскакивает на ещё живом токене из снимка кук, всё последующее упирается в непройденный челлендж. Это ровно #3045 — «QRATOR отдаёт proof-of-work челлендж, а мы его не проходим ни одним из путей». Тот же вендор защиты, тот же отказ, только там он вскрылся на Авито. **Корень общий для двух поставщиков**, и решать его надо там, а не в домкликовской частности. ## Что делать с уже влитым Откатывать #3193 не предлагаю: правка opt-in, для остальных трёх поставщиков payload не меняется, тесты зелёные, а тёплый контекст сам по себе корректнее прежнего поведения и понадобится, когда челлендж научимся проходить. Но она **не закрывает эту задачу** — снимаю с неё эту роль. Важное следствие для #3197: ключ контекста по провайдеру безопасен ровно до тех пор, пока ДомКлик ходит мимо пула. Подключение пула без смены ключа даст новую поломку. Задача остаётся открытой, корневая причина переезжает в #3045.
Author
Owner

Снял сырой HTML блокирующего ответа. Это не челлендж и не бан — это 403

Предыдущий комментарий предполагал непройденный PoW-челлендж QRATOR по аналогии с #3045. Это неверно. Снял сырьё: пять bf.fetch() подряд в одном тёплом контексте, пауза 7 с, прогрев origin включён.

# Размер __SSR_STATE__ <title>
1 807 869 да настоящая карточка
2 26 624 нет 403 | Домклик
3 26 624 нет 403 | Домклик
4 26 624 нет 403 | Домклик
5 26 624 нет 403 | Домклик

Проверка маркеров на этой странице: startpow — нет, доступ ограничен: проверка безопасности — нет, доступ ограничен: проблема с ip — нет, qrator — нет, captcha — нет.

То есть PoW-скрипта там нет вовсе, ждать нечего, _wait_out_pow_challenge тут неприменима по существу. Это статическая страница отказа фиксированного размера.

Что это значит

Отпадают все три версии подряд: протухший qrator_jsid2 (тёплый контекст его починил, эффекта ноль), репутация exit-IP (свежеротированный блокировался с первого запроса), непройденный челлендж (челленджа нет).

Остаётся ровно одно, и оно согласуется со всеми замерами: ДомКлик держит жёсткий лимит примерно в одну карточку на сессию для нашего egress. Те же карточки с домашнего резидентного канала идут пять из пяти с паузой 2 с — оттуда лимит не срабатывает. Разница не в браузере и не в куках, а в сети, из которой мы приходим.

Дыра, из-за которой это две недели выглядело как бан

Сайдкар не смотрит HTTP-статус навигации вообще. _fetch_once делает page.goto(...), игнорирует возвращённый Response и классифицирует результат только по текстовым маркерам в HTML (_is_ban_page / _is_pow_challenge), причём оба списка сняты с авитовских страниц.

Честный 403 ни в один список не попадает, поэтому 26-килобайтная страница ошибки отдаётся наверх как валидный контент. Дальше парсер ДомКлика не находит __SSR_STATE__ и поднимает DomClickBlockedError — а тот, в свою очередь, схлопывает anti-bot маркер, разлогин и сетевой сбой в один тип. Отсюда 14 прогонов подряд с ban_kind=unknown: причина была прямо в статусе ответа, мы её просто не читали.

Следствия

  1. Проверять статус ответа page.goto в сайдкаре — правка на несколько строк, даёт честную классификацию сразу и без интерпретации. Дописал в #3196 (A1), там это и место.
  2. Настоящее решение сбора — егресс, а не код: резидентные адреса вместо текущих, либо радикально более медленный темп. Это уже не про #3190.
  3. reuse_context из #3193 к этому отношения не имеет и остаётся как есть — правка корректная, просто лечила не ту болезнь.

Тикет остаётся открытым, но переформулирую: корневая причина — не сессия и не контекст, а лимит на стороне площадки плюс наша неспособность прочитать 403.

## Снял сырой HTML блокирующего ответа. Это не челлендж и не бан — это 403 Предыдущий комментарий предполагал непройденный PoW-челлендж QRATOR по аналогии с #3045. **Это неверно.** Снял сырьё: пять `bf.fetch()` подряд в одном тёплом контексте, пауза 7 с, прогрев origin включён. | # | Размер | `__SSR_STATE__` | `<title>` | |---|---|---|---| | 1 | 807 869 | да | настоящая карточка | | 2 | 26 624 | нет | `403 \| Домклик` | | 3 | 26 624 | нет | `403 \| Домклик` | | 4 | 26 624 | нет | `403 \| Домклик` | | 5 | 26 624 | нет | `403 \| Домклик` | Проверка маркеров на этой странице: `startpow` — нет, `доступ ограничен: проверка безопасности` — нет, `доступ ограничен: проблема с ip` — нет, `qrator` — нет, `captcha` — нет. То есть PoW-скрипта там нет вовсе, ждать нечего, `_wait_out_pow_challenge` тут неприменима по существу. Это статическая страница отказа фиксированного размера. ## Что это значит Отпадают все три версии подряд: протухший `qrator_jsid2` (тёплый контекст его починил, эффекта ноль), репутация exit-IP (свежеротированный блокировался с первого запроса), непройденный челлендж (челленджа нет). Остаётся ровно одно, и оно согласуется со всеми замерами: **ДомКлик держит жёсткий лимит примерно в одну карточку на сессию для нашего egress**. Те же карточки с домашнего резидентного канала идут пять из пяти с паузой 2 с — оттуда лимит не срабатывает. Разница не в браузере и не в куках, а в сети, из которой мы приходим. ## Дыра, из-за которой это две недели выглядело как бан Сайдкар **не смотрит HTTP-статус навигации вообще**. `_fetch_once` делает `page.goto(...)`, игнорирует возвращённый Response и классифицирует результат только по текстовым маркерам в HTML (`_is_ban_page` / `_is_pow_challenge`), причём оба списка сняты с авитовских страниц. Честный `403` ни в один список не попадает, поэтому 26-килобайтная страница ошибки отдаётся наверх как валидный контент. Дальше парсер ДомКлика не находит `__SSR_STATE__` и поднимает `DomClickBlockedError` — а тот, в свою очередь, схлопывает anti-bot маркер, разлогин и сетевой сбой в один тип. Отсюда 14 прогонов подряд с `ban_kind=unknown`: причина была прямо в статусе ответа, мы её просто не читали. ## Следствия 1. **Проверять статус ответа `page.goto` в сайдкаре** — правка на несколько строк, даёт честную классификацию сразу и без интерпретации. Дописал в #3196 (A1), там это и место. 2. Настоящее решение сбора — егресс, а не код: резидентные адреса вместо текущих, либо радикально более медленный темп. Это уже не про #3190. 3. `reuse_context` из #3193 к этому отношения не имеет и остаётся как есть — правка корректная, просто лечила не ту болезнь. Тикет остаётся открытым, но переформулирую: корневая причина — не сессия и не контекст, а лимит на стороне площадки плюс наша неспособность прочитать `403`.
Collaborator

Закрываю: PR #3193 смержен, приёмка 28.08 показала, что гипотеза протухших кук неверна (блок — чистый 403 без челленджа). Продолжение работы разнесено по живым: #3246 (бан узла вместо сброса контекста), #3251 (методика-чеклист по площадкам), #3263 (заход через поиск). Ревизия 01.09.2026.

Закрываю: PR #3193 смержен, приёмка 28.08 показала, что гипотеза протухших кук неверна (блок — чистый 403 без челленджа). Продолжение работы разнесено по живым: #3246 (бан узла вместо сброса контекста), #3251 (методика-чеклист по площадкам), #3263 (заход через поиск). Ревизия 01.09.2026.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#3190
No description provided.