fix(tradein/scraper): не ходить через мёртвый запасной прокси #2634
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2634
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/tradein-no-dead-env-proxy-fallback"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Issue #2616 шаг 1. Убирает падение сборщиков на мёртвый запасной (env) прокси — измеренная причина 73-98% событий
bannedза 30 дней (issue #2613/#2616 диагностика).Что сделано (3 файла из issue + плюс
contracts.py/scraper_adapters.py/_base.pyдля проброса признака окружения):packages/scraper-kit/src/scraper_kit/providers/_proxy.py::curl_proxy_url— когда пул реально задействован (use_proxy_pool_curl=True,ProxyProviderпередан) иacquire()вернулNone/упал в prod — поднимаетNoProxyAvailableErrorДО HTTP-запроса вместо похода через мёртвыйenv_fallback_url. В dev (или когдаconfig/proxy_providerне заданы — существующий dev/no-op путь,providers/cian/detail.py:118) поведение не меняется.packages/scraper-kit/src/scraper_kit/browser_fetcher.py::_pool_proxy— то же для browser-пути: пул on + пуст + prod →NoProxyAvailableError, POST/fetch//fetch-jsonне отправляется.tradein-mvp/browser/server.py— новый guard_no_live_proxy()вfetch_handler/fetch_json_handler/login_handler: prod + нетproxyв теле запроса И нет ЛЮБОЙ env-переменной для провайдера → явный отказ (503,"no proxy configured — refusing direct connection (prod)") ДО лока/launch'а camoufox, вместо тихого запуска браузера без прокси (= прямое подключение с IP сервера). Это прицельная защита от следующего шага issue #2616 (удаление мёртвых env-переменных) — без неё после удаления env camoufox бы тихо стал ходить с IP сервера.ScraperConfig.environment: str(Protocol,contracts.py) ←RealScraperConfig.environment←settings.environment(уже существовавшее поле, дефолт"dev", прод ="production").server.pyчитаетENVIRONMENTиз os.environ напрямую (это отдельный процесс/контейнер, не FastAPI-приложение).NoProxyAvailableError(наша инфраструктура, structured.providerатрибут, текст без "blocked"/"captcha"/"banned") vsAvitoBlockedError/AvitoRateLimitedError(блокировка площадкой) vs голыеhttpx/curl_cffitransport-исключения (сетевая ошибка).server.pyотдаёт разныйerror-текст для "нет прокси вообще" vs "launch упал на заданном прокси" (browser unavailable (proxy may be down), существующий путь — не тронут).Границы (по ТЗ):
proxy_pool.py/proxy_rotation.pyне тронуты. Env-переменные НЕ удалены (отдельный шаг #2616, после этого PR). Расписания/пейсинг не менялись. Миграций нет.Оценка эффекта (read-only,
scrape_runs, последние 14 дней)53
banned-прогона за 14 дней — все Avito,cian_*/yandex_*/domclick_*= 0 banned. Разбивка по error-тексту: 52/53 ="...browser unavailable (proxy may be down)", 1/53 = реальный firewall-детект ("Avito SERP firewall (browser-mode) — IP banned").Честно про прямой эффект этого PR на счётчик "banned" за следующие 14 дней: ~0. Причина — Avito browser-SERP путь (
providers/avito/serp.py::_fetch_serp_html_browser) сегодня НЕ проброшен наProxyProvider(build_browser_fetcher(config, "avito")вызывается безproxy_provider=, в отличие от cian/yandex) — значит_pool_proxyу него всегдаuse_pool=False, и этот PR (гейтится наuse_pool=True) его не касается. Плюс сегодняшниеBROWSER_PROXY_AVITO/AVITO_PROXY_URL— мёртвые, но заданные переменные (407/connection refused, не отсутствующие) — guard #4 (_no_live_proxy) специально не триггерится на "задано, но дохлое": удаление дохлых переменных — намеренно отдельный, следующий шаг issue #2616 (я его не делаю в этом PR, по ТЗ). После этого шага без guard #4 avito тихо пошёл бы напрямую с IP сервера; с guard #4 — получит явный отказ вместо этого.Что этот PR РЕАЛЬНО меняет сегодня: убирает скрытый риск wasted-request/kill+relaunch цикла и мёртвого-env-похода на cian/yandex curl+browser путях (
use_proxy_pool_curl/use_proxy_pool_browser=trueв проде, пул иногда может быть пуст) — сейчас там 0 banned, но при опустошении пула они бы тихо пошли на дохлыеCIAN_PROXY_URL/YANDEX_PROXY_URL/BROWSER_PROXY_CIAN/BROWSER_PROXY_YANDEX(тоже дохлые, см. issue) вместо явного отказа.Рекомендованный следующий шаг (НЕ в этом PR, другой scope): чтобы реально снять маркировку
bannedс 52/53 avito-прогонов, нужно (a) либо wiringproxy_provider=в avito browser-путь + классификацияNoProxyAvailableErrorотдельно отis_soft_banвavito/serp.py:535-540(сейчас ЛЮБОЙstatus==503/"browser unavailable" безусловно трактуется как soft-ban → после исчерпания бюджета ротации →mark_banned), либо (b) шаг 2 issue #2616 (удаление дохлых env) — тогда сегодняшний launch-fail (503) исчезнет сам, camoufox либо получит признак "нет прокси" через guard #4 (503, distinguishable), либо (при отключённом guard) пойдёт напрямую — что как раз и предотвращает этот PR.Test plan
cd tradein-mvp/backend && uv run pytest -q— 3190 passed, 9 skipped, 1 known pre-existing fail (tests/test_search_api.py::test_search_cache_hit, 401 RBAC — не в scope, явно указан в ТЗ).cd tradein-mvp/browser && uv run --no-project --with aiohttp --with pytest pytest test_server_no_proxy_refusal.py -q— 10/10 passed (новый файл, изолированный тест-раннер как у соседнихtest_server_*.py—browser/не имеетpyproject.toml, зависимостиaiohttp/pytestтолько в Docker-образе).ruff check(project selectE,F,I,B,UP,N,RUF) — чисто на всех изменённых файлах.browser/test_server_smoke.py/test_server.pyимеют 4 pre-existing failures (устаревшие моки_ensure_browserбезproxy_overridekwarg) — воспроизведены наgit stash(код ДО этого PR), не вызваны этим PR.Falsification (числа обоих прогонов)
curl_proxy_url/_pool_proxyprod-refusal тесты (test_flag_on_empty_pool_prod_refuses_no_http_request,test_fetch_pool_empty_prod_refuses_no_http_postи т.д.): заглушка_boom()/client.postкидаетAssertionErrorна любом вызове внутриwith-блока / на POST. С фиксом —NoProxyAvailableErrorподнимается ДОyield,_boom()/client.postне достигается →pytest.raises(NoProxyAvailableError)матчит.pytest tests/test_proxy_pool_curl_paths.py tests/test_kit_browser_fetcher_proxy_pool.py -q→ 51 passed.curl_proxy_url/_pool_proxyна pre-fix реализацию, всегдаyield env_fallback_urlпри пустом пуле): новые prod-тесты падают на несовпадении типа исключения (AssertionErrorвместоNoProxyAvailableError) — подтверждено построчным разбором логики: raise стоит ПОСЛЕ проверкиuse_pool and _is_prod(config), при revert она снята →_boom()/client.postвыполняются → тест ловит несовпадение типа исключения.Файлы
tradein-mvp/packages/scraper-kit/src/scraper_kit/proxy_errors.py(новый) —NoProxyAvailableErrortradein-mvp/packages/scraper-kit/src/scraper_kit/providers/_proxy.pytradein-mvp/packages/scraper-kit/src/scraper_kit/browser_fetcher.pytradein-mvp/packages/scraper-kit/src/scraper_kit/providers/_base.pytradein-mvp/packages/scraper-kit/src/scraper_kit/contracts.pytradein-mvp/backend/app/services/scraper_adapters.pytradein-mvp/browser/server.pytradein-mvp/backend/tests/test_proxy_pool_curl_paths.py,test_kit_browser_fetcher_proxy_pool.py,test_kit_serp_proxy_pool.py(fixup: recording-стаб принял новый kwarg),test_scraper_adapters_contracts.py,test_scraper_kit_providers_base.py,tradein-mvp/browser/test_server_no_proxy_refusal.py(новый)Refs #2616
done#2698