fix(avito): считать HTTP 439 блокировкой, а не концом выдачи #3065

Merged
lekss361 merged 1 commit from fix/avito-439-and-context into main 2026-08-23 21:49:36 +00:00
Owner

Топ-1 находка аудита скрапперов Avito/Cian (24.08).

Проблема

439 — нестандартный код, которым QRATOR отвечает на странице-челлендже Avito. Его нет ни в одном RFC, поэтому он не попал ни в один список «это блок» — и обе точки входа трактовали его как штатный исход:

Точка Что было Как читалось
SERP (serp.py) проваливался в if sc != 200: return None «страницы кончились» — пагинация мирно останавливается, ни ротации IP, ни _report_ban, ни пометки прогона banned
detail (detail.py) доходил до if sc != 200: raise ValueError «не смог распарсить страницу» — без реконнекта на свежий exit-IP и без ban-сигнала

Обе ветки молчали ровно там, где нас блокировали. Отсюда прогоны с диагнозом «причина НЕ установлена» (#2700) при том, что у отказа был вполне чёткий HTTP-код: за 14 дней 43 из 82 прогонов avito-семейства (52 %) завершились failed / banned, и часть из них — именно этот класс тихого отказа.

Ортогонально уже сделанному #3046 (тот учил браузер дожидаться решения челленджа) — 439 всё ещё прилетает на curl_cffi-путях: SERP-фолбэк _fetch_serp_html и detail, где код-дефолт avito_detail_backfill_use_curl=True переопределён только прод-compose'ом.

Решение

439 подключён к уже существующей 403-ветке, а не новым механизмом: тот же смысл (текущий exit-IP признан подозрительным), то же лечение (смена IP, а не повтор на той же сессии).

  • serp.py — в условие блока (sc in (403, 429, 439)) и в backconnect-reconnect (sc in (403, 439) or is_firewall)
  • detail.py — в блок-ветку (sc in (403, 439) or is_firewall)

Заодно текст AvitoBlockedError в detail.py перестал жёстко печатать "403" — на 439 он врал бы в логах ровно там, где по логу и ставят диагноз.

Test plan

  • Фальсификация на коде до правки: 403-кейсы проходят, 439-кейсы падают — SERP DID NOT RAISE, detail ValueError: avito detail HTTP 439. То есть тест ловит именно эту регрессию, а не проходит «сам по себе».
  • Регрессия: 421 passed, 2 skipped (-k "avito or ban_kind or proxy_pool or browser_routing").

Тест test_3044 параметризован по [403, 439] намеренно — не ради полноты, а чтобы стеречь паритет: 403 заведомо рабочая ветка, и если кто-то перепишет блок-логику и уронит только 439, упадёт половина кейсов и сразу покажет, что ветки разъехались.

Чего здесь НЕТ

#3035 (обрезка ?context= в avito_detail_backfill.py:433) сознательно не вошла. Проверка на проде показала, что вопрос не такой простой, как выглядел — подробности комментарием в #3035.

Refs #3044, #2700

Топ-1 находка аудита скрапперов Avito/Cian (24.08). ## Проблема **439** — нестандартный код, которым QRATOR отвечает на странице-челлендже Avito. Его нет ни в одном RFC, поэтому он не попал ни в один список «это блок» — и обе точки входа трактовали его как штатный исход: | Точка | Что было | Как читалось | |---|---|---| | SERP (`serp.py`) | проваливался в `if sc != 200: return None` | «страницы кончились» — пагинация мирно останавливается, ни ротации IP, ни `_report_ban`, ни пометки прогона `banned` | | detail (`detail.py`) | доходил до `if sc != 200: raise ValueError` | «не смог распарсить страницу» — без реконнекта на свежий exit-IP и без ban-сигнала | Обе ветки молчали ровно там, где нас блокировали. Отсюда прогоны с диагнозом *«причина НЕ установлена»* (#2700) при том, что у отказа был вполне чёткий HTTP-код: за 14 дней **43 из 82** прогонов avito-семейства (52 %) завершились `failed` / `banned`, и часть из них — именно этот класс тихого отказа. Ортогонально уже сделанному #3046 (тот учил браузер **дожидаться** решения челленджа) — 439 всё ещё прилетает на curl_cffi-путях: SERP-фолбэк `_fetch_serp_html` и detail, где код-дефолт `avito_detail_backfill_use_curl=True` переопределён только прод-compose'ом. ## Решение 439 подключён к **уже существующей 403-ветке**, а не новым механизмом: тот же смысл (текущий exit-IP признан подозрительным), то же лечение (смена IP, а не повтор на той же сессии). - `serp.py` — в условие блока (`sc in (403, 429, 439)`) и в backconnect-reconnect (`sc in (403, 439) or is_firewall`) - `detail.py` — в блок-ветку (`sc in (403, 439) or is_firewall`) Заодно текст `AvitoBlockedError` в `detail.py` перестал жёстко печатать `"403"` — на 439 он врал бы в логах ровно там, где по логу и ставят диагноз. ## Test plan - [x] **Фальсификация на коде до правки:** 403-кейсы проходят, 439-кейсы падают — SERP `DID NOT RAISE`, detail `ValueError: avito detail HTTP 439`. То есть тест ловит именно эту регрессию, а не проходит «сам по себе». - [x] **Регрессия:** `421 passed, 2 skipped` (`-k "avito or ban_kind or proxy_pool or browser_routing"`). Тест `test_3044` параметризован по `[403, 439]` намеренно — не ради полноты, а чтобы стеречь **паритет**: 403 заведомо рабочая ветка, и если кто-то перепишет блок-логику и уронит только 439, упадёт половина кейсов и сразу покажет, что ветки разъехались. ## Чего здесь НЕТ **#3035** (обрезка `?context=` в `avito_detail_backfill.py:433`) сознательно **не** вошла. Проверка на проде показала, что вопрос не такой простой, как выглядел — подробности комментарием в #3035. Refs #3044, #2700
lekss361 added 1 commit 2026-08-23 21:21:08 +00:00
fix(avito): считать HTTP 439 блокировкой, а не концом выдачи
All checks were successful
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Successful in 4m26s
5d0206d972
439 — нестандартный код, которым QRATOR отвечает на странице-челлендже
Avito. Его нет ни в одном RFC, поэтому он не попал ни в один список
«это блок» — и обе точки входа трактовали его как штатный исход:

  SERP   проваливался в `if sc != 200: return None` — пагинация мирно
         останавливалась, будто страницы кончились. Ни ротации IP, ни
         _report_ban, ни пометки прогона banned.
  detail доходил до `if sc != 200: raise ValueError` — то есть считался
         ошибкой разбора страницы, без реконнекта на свежий exit-IP и
         без ban-сигнала.

То есть обе ветки молчали ровно там, где нас блокировали. Отсюда
прогоны с диагнозом «причина НЕ установлена» (#2700) при том, что у
отказа был вполне чёткий HTTP-код: за 14 дней 43 из 82 прогонов
avito-семейства (52%) завершились failed/banned, и часть из них —
именно этот класс тихого отказа.

439 подключён к УЖЕ СУЩЕСТВУЮЩЕЙ 403-ветке, а не новым механизмом:
тот же смысл (текущий exit-IP признан подозрительным), то же лечение
(смена IP, а не повтор на той же сессии). В serp.py — в условие
блока и в backconnect-reconnect; в detail.py — в блок-ветку.

Заодно текст AvitoBlockedError в detail.py перестал жёстко печатать
"403": на 439 он врал бы в логах ровно там, где по логу и ставят
диагноз.

Тест test_3044 параметризован по [403, 439] намеренно — не ради
полноты, а чтобы стеречь ПАРИТЕТ: 403 заведомо рабочая ветка, и если
кто-то перепишет блок-логику и уронит только 439, упадёт половина
кейсов и сразу покажет, что ветки разъехались.

Проверено:
  - фальсификация на коде до правки: 403-кейсы проходят, 439-кейсы
    падают (SERP «DID NOT RAISE», detail «ValueError: HTTP 439»)
  - регрессия: 421 passed, 2 skipped (-k "avito or ban_kind or
    proxy_pool or browser_routing")

#3035 (обрезка ?context= в avito_detail_backfill) в этот коммит НЕ
входит — см. комментарий в issue: замер показал, что query несут все
55 140 avito-URL, а та же обрезка стоит ещё в двух местах kit'а, и
знак эффекта от её снятия неизвестен.

Refs #3044, #2700
lekss361 merged commit 3b6bca80c0 into main 2026-08-23 21:49:36 +00:00
lekss361 deleted branch fix/avito-439-and-context 2026-08-23 21:49:37 +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#3065
No description provided.