"""Cian-specific exceptions для anti-bot detection.""" from scraper_kit.proxy_errors import ProxyBanError class CianBlockedError(ProxyBanError): """HTTP 403 от Циана — узел, с которого мы пришли, отбит WAF'ом площадки. Живая различающая проба 2026-08-10 (#2700), один и тот же detail-URL, один и тот же код, менялся ТОЛЬКО прокси-узел: узел 1 (asocks-residential, 46.8.110.92) → HTTP 403, 21 564 байт, маркер `cian_waf_block` узел 9 (asocks-mobile-1, 5.227.16.0) → HTTP 200, 617 352 байт, state ok узел 10 (asocks-mobile-2, 95.104.183.29) → HTTP 200, 617 355 байт, state ok узел 11 (asocks-mobile-3, 95.55.49.98) → HTTP 200, 617 407 байт, state ok То есть 403 — свойство ПАРЫ «узел × Циан», а не площадки вообще и не нашего запроса: detail-страница Циана авторизации не требует и отдаётся без единой куки (проба выше шла без них). Поэтому исключение наследует `ProxyBanError` — тот же приём, что у `AvitoBlockedError`/`DomClickBlockedError`: generic curl-слой (`providers/_proxy.py::curl_proxy_url`) увидит `isinstance(exc, ProxyBanError)` и снимет узел с выдачи ИМЕННО Циану (per-source бан, #2600 п.2), не трогая остальные источники. Почему это заводится отдельным исключением, а не остаётся `return None`: пока 403 гасился внутри `fetch_detail`, наружу не выходило НИЧЕГО — пул получал `mark_health(ok=True)` на отбитый узел и продолжал выдавать его Циану, а прогон писал `detail_failed=50` при `errors_count=0` и статусе `done` (#2700: 15 суток подряд, 50 из 50 отказов ежедневно). 404 сюда НЕ относится: удалённое объявление — не бан, узел за него наказывать нельзя. Остальные не-200 остаются прежним мягким отказом (`None` + WARNING). """