fix(browser-sidecar): бан-страница площадки отдаётся 403, а не 500 (#3288 п.C) #3379
6 changed files with 187 additions and 42 deletions
|
|
@ -1,7 +1,8 @@
|
|||
"""#3283: бан-страница Авито («доступ ограничен: проблема с IP») приходит от
|
||||
сайдкара как SidecarBanPageError (HTTP 500 + ban_page-маркер в теле, #3239), но
|
||||
у Авито её никто не читал — она заворачивалась в AvitoSidecarUnavailableError и
|
||||
уезжала в транспортные сбои ('infra'), а не в отказ площадки ('platform').
|
||||
сайдкара как SidecarBanPageError (ban_page-маркер в теле, #3239; код ответа — 403
|
||||
с #3288 п.4, 500 у сайдкара старой сборки), но у Авито её никто не читал — она
|
||||
заворачивалась в AvitoSidecarUnavailableError и уезжала в транспортные сбои
|
||||
('infra'), а не в отказ площадки ('platform').
|
||||
|
||||
Прод-замер (прогон 5606): 64 бан-страницы, ban_kinds={"infra":26,"platform":7}, при
|
||||
этом ни ОДНОЙ записи для source=avito в scrape_proxy_source_bans — узел не банился и
|
||||
|
|
@ -26,7 +27,7 @@ import pytest
|
|||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
|
||||
|
||||
from scraper_kit.avito_exceptions import AvitoBlockedError, AvitoSidecarUnavailableError
|
||||
from scraper_kit.browser_fetcher import SidecarBanPageError
|
||||
from scraper_kit.browser_fetcher import SidecarBanPageError, _raise_for_sidecar_status
|
||||
from scraper_kit.providers.avito import serp as serp_module
|
||||
from scraper_kit.providers.avito.detail import fetch_detail
|
||||
from scraper_kit.providers.avito.serp import AvitoScraper
|
||||
|
|
@ -36,16 +37,40 @@ from app.services.scraper_adapters import RealScraperConfig
|
|||
_ITEM_URL = "https://www.avito.ru/ekaterinburg/kvartiry/x_1234567890"
|
||||
|
||||
|
||||
def _sidecar_ban_page_error(upstream_status: int | None = 500) -> SidecarBanPageError:
|
||||
"""Ровно то, что сайдкар шлёт на подтверждённую бан-страницу («доступ ограничен:
|
||||
проблема с IP») — зеркало _sidecar_ban_page_error из test_domclick_detail.py."""
|
||||
request = httpx.Request("POST", "http://tradein-browser:3000/fetch")
|
||||
response = httpx.Response(500, request=request)
|
||||
return SidecarBanPageError(
|
||||
"Server error '500' | tradein-browser: BanPageDetectedError: доступ ограничен",
|
||||
request=request,
|
||||
response=response,
|
||||
upstream_status=upstream_status,
|
||||
_SIDECAR_BAN_CODES = [403, 500]
|
||||
|
||||
|
||||
def _sidecar_ban_page_error(
|
||||
sidecar_status: int = 403, *, upstream_status: int | None = 403
|
||||
) -> SidecarBanPageError:
|
||||
"""Ровно то, что клиент поднимает на подтверждённой бан-странице («доступ
|
||||
ограничен: проблема с IP») — зеркало _sidecar_ban_page_error из test_domclick_detail.py.
|
||||
|
||||
Исключение строим НАСТОЯЩИМ `_raise_for_sidecar_status` из ответа сайдкара, а не
|
||||
литералом: текст менялся (#3288 п.4 убрал httpx-преамбулу, и захардкоженная строка
|
||||
«Server error '500' | tradein-browser: …» пережила фикс, которого уже нет в коде).
|
||||
|
||||
`sidecar_status` — код ОТВЕТА САЙДКАРА: 403 с #3288 п.4, 500 у сайдкара старой
|
||||
сборки (`tradein-browser` — отдельный образ со своим шагом деплоя, версии штатно
|
||||
разъезжаются на часы). Признак бана — ТЕЛО, поэтому оба кода обязаны дать один и
|
||||
тот же тип; прогон по `_SIDECAR_BAN_CODES` это и проверяет.
|
||||
`upstream_status` — код, который площадка отдала браузеру.
|
||||
"""
|
||||
response = httpx.Response(
|
||||
sidecar_status,
|
||||
json={
|
||||
"error": "BanPageDetectedError: доступ ограничен",
|
||||
"ban_page": True,
|
||||
"status": upstream_status,
|
||||
},
|
||||
request=httpx.Request("POST", "http://tradein-browser:3000/fetch"),
|
||||
)
|
||||
try:
|
||||
_raise_for_sidecar_status(response)
|
||||
except SidecarBanPageError as exc:
|
||||
return exc
|
||||
raise AssertionError(
|
||||
f"ответ сайдкара {sidecar_status} с ban_page обязан дать SidecarBanPageError"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -53,12 +78,15 @@ def _sidecar_ban_page_error(upstream_status: int | None = 500) -> SidecarBanPage
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_detail_sidecar_ban_page_raises_platform_block_not_infra() -> None:
|
||||
@pytest.mark.parametrize("sidecar_status", _SIDECAR_BAN_CODES)
|
||||
async def test_fetch_detail_sidecar_ban_page_raises_platform_block_not_infra(
|
||||
sidecar_status: int,
|
||||
) -> None:
|
||||
"""Бан-страница — ГЕНУИННЫЙ отказ площадки: AvitoBlockedError, не
|
||||
AvitoSidecarUnavailableError. Фальсификация: до фикса SidecarBanPageError падала
|
||||
в `except Exception` (она подкласс httpx.HTTPStatusError) и уезжала сюда."""
|
||||
bf = MagicMock()
|
||||
bf.fetch = AsyncMock(side_effect=_sidecar_ban_page_error())
|
||||
bf.fetch = AsyncMock(side_effect=_sidecar_ban_page_error(sidecar_status))
|
||||
|
||||
with pytest.raises(AvitoBlockedError) as excinfo:
|
||||
await fetch_detail(_ITEM_URL, browser_fetcher=bf)
|
||||
|
|
@ -67,7 +95,10 @@ async def test_fetch_detail_sidecar_ban_page_raises_platform_block_not_infra() -
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_detail_sidecar_ban_page_does_not_report_ban_again() -> None:
|
||||
@pytest.mark.parametrize("sidecar_status", _SIDECAR_BAN_CODES)
|
||||
async def test_fetch_detail_sidecar_ban_page_does_not_report_ban_again(
|
||||
sidecar_status: int,
|
||||
) -> None:
|
||||
"""Бан рапортует ФЕТЧЕР (`_report_platform_ban`), провайдер — уже нет (#3288).
|
||||
|
||||
До #3288 здесь стоял `assert_called_once()`, и это было верно, пока фетчер о
|
||||
|
|
@ -78,7 +109,7 @@ async def test_fetch_detail_sidecar_ban_page_does_not_report_ban_again() -> None
|
|||
BrowserFetcher с фейковым пулом, а не MagicMock).
|
||||
"""
|
||||
bf = MagicMock()
|
||||
bf.fetch = AsyncMock(side_effect=_sidecar_ban_page_error())
|
||||
bf.fetch = AsyncMock(side_effect=_sidecar_ban_page_error(sidecar_status))
|
||||
|
||||
with pytest.raises(AvitoBlockedError):
|
||||
await fetch_detail(_ITEM_URL, browser_fetcher=bf)
|
||||
|
|
@ -105,7 +136,10 @@ async def test_fetch_detail_transport_failure_stays_infra_no_report_ban() -> Non
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_serp_sidecar_ban_page_raises_platform_block_without_retrying() -> None:
|
||||
@pytest.mark.parametrize("sidecar_status", _SIDECAR_BAN_CODES)
|
||||
async def test_serp_sidecar_ban_page_raises_platform_block_without_retrying(
|
||||
sidecar_status: int,
|
||||
) -> None:
|
||||
"""Бан-страница у SERP → AvitoBlockedError + report_ban, БЕЗ ретраев.
|
||||
|
||||
Детект маркерный (SidecarBanPageError.__doc__), переспрашивать его нечем, а
|
||||
|
|
@ -115,7 +149,7 @@ async def test_serp_sidecar_ban_page_raises_platform_block_without_retrying() ->
|
|||
"""
|
||||
scraper = AvitoScraper(RealScraperConfig())
|
||||
scraper._browser = AsyncMock()
|
||||
scraper._browser.fetch = AsyncMock(side_effect=_sidecar_ban_page_error())
|
||||
scraper._browser.fetch = AsyncMock(side_effect=_sidecar_ban_page_error(sidecar_status))
|
||||
scraper._browser.report_ban = MagicMock() # sync-хук, не корутина (см. test_2686)
|
||||
|
||||
sleep_mock = AsyncMock()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
(`ProxyPoolExhaustedError pool_total=7 banned_for_source=0 unhealthy_or_disabled=7`),
|
||||
при том что живая проба тех же узлов проходила — они стояли ровно на потолке
|
||||
`MAX_CONSECUTIVE_FAILS`. Механизм: сайдкар на бан-странице Авито («доступ ограничен:
|
||||
проблема с IP») отвечает HTTP 500 с `ban_page: true`, клиент поднимает
|
||||
проблема с IP») отвечает `ban_page: true` в теле (кодом 403 с #3288 п.4, кодом 500 у
|
||||
сайдкара старой сборки), клиент поднимает
|
||||
`SidecarBanPageError` — но она подкласс `httpx.HTTPStatusError`, и общий
|
||||
`except Exception` в `_post_fetch` звал `_report_fetch_result(False)` →
|
||||
`mark_health(ok=False)`. `mark_health` — решение ГЛОБАЛЬНОЕ для пула: три бан-страницы
|
||||
|
|
@ -87,13 +88,22 @@ class _FakePool:
|
|||
self.banned.append((lease.id, source))
|
||||
|
||||
|
||||
def _ban_page_response() -> httpx.Response:
|
||||
"""РОВНО то, что отдаёт сайдкар на распознанной бан-странице (browser/server.py:1356):
|
||||
HTTP 500 + `ban_page: true` + статус целевой навигации. Ответ настоящий, а не мок, —
|
||||
чтобы проба шла тем же трактом, что и прод (_raise_for_sidecar_status разбирает тело)."""
|
||||
_SIDECAR_BAN_CODES = [403, 500]
|
||||
|
||||
|
||||
def _ban_page_response(sidecar_status: int = 403) -> httpx.Response:
|
||||
"""РОВНО то, что отдаёт сайдкар на распознанной бан-странице (browser/server.py):
|
||||
`ban_page: true` + статус целевой навигации. Ответ настоящий, а не мок, — чтобы
|
||||
проба шла тем же трактом, что и прод (_raise_for_sidecar_status разбирает тело).
|
||||
|
||||
`sidecar_status` — код ОТВЕТА САЙДКАРА, а не площадки: 403 с #3288 п.4, 500 у
|
||||
сайдкара старой сборки. `tradein-browser` — отдельный образ со своим шагом
|
||||
деплоя, поэтому версии сайдкара и backend штатно разъезжаются на часы; оба кода
|
||||
обязаны давать один диагноз, и тесты ниже прогоняются по `_SIDECAR_BAN_CODES`.
|
||||
"""
|
||||
request = httpx.Request("POST", "http://browser:3000/fetch")
|
||||
return httpx.Response(
|
||||
500,
|
||||
sidecar_status,
|
||||
json={
|
||||
"error": "BanPageDetectedError: бан-страница (проблема с IP)",
|
||||
"ban_page": True,
|
||||
|
|
@ -151,15 +161,22 @@ def _iter_causes(exc: BaseException) -> list[BaseException]:
|
|||
# ── (а) бан-страница → отказ ПЛОЩАДКИ + адресный бан пары «узел×источник» ──────
|
||||
|
||||
|
||||
async def test_ban_page_raises_platform_block_and_bans_node_for_avito_only() -> None:
|
||||
@pytest.mark.parametrize("sidecar_status", _SIDECAR_BAN_CODES)
|
||||
async def test_ban_page_raises_platform_block_and_bans_node_for_avito_only(
|
||||
sidecar_status: int,
|
||||
) -> None:
|
||||
"""Бан-страница: AvitoBlockedError, диагноз 'platform', mark_banned(13, 'avito').
|
||||
|
||||
Фальсификация: до фикса SidecarBanPageError доезжала до общего except в
|
||||
_post_fetch → mark_health(ok=False), а строки в scrape_proxy_source_bans не
|
||||
появлялось вовсе (за 7 суток 0 записей для source=avito при 58 прогонах banned).
|
||||
Вторая фальсификация (#3288 п.4): заведи в `_sidecar_ban_page_status` гейт
|
||||
«признавать ban_page только при 500» — 403-кейс покраснеет по значению
|
||||
(AvitoSidecarUnavailableError вместо бана), т.е. в час рассинхрона версий
|
||||
отказ площадки снова уезжал бы в инфра-ветку.
|
||||
"""
|
||||
pool = _FakePool()
|
||||
bf = await _fetcher(_ban_page_response(), pool)
|
||||
bf = await _fetcher(_ban_page_response(sidecar_status), pool)
|
||||
|
||||
with patch("scraper_kit.browser_fetcher.asyncio.sleep", AsyncMock()):
|
||||
with pytest.raises(AvitoBlockedError) as ei:
|
||||
|
|
@ -176,14 +193,15 @@ async def test_ban_page_raises_platform_block_and_bans_node_for_avito_only() ->
|
|||
# ── (б) здоровье узла: бан-страница молчит, транспортный сбой — говорит ────────
|
||||
|
||||
|
||||
async def test_ban_page_does_not_mark_health_failure() -> None:
|
||||
@pytest.mark.parametrize("sidecar_status", _SIDECAR_BAN_CODES)
|
||||
async def test_ban_page_does_not_mark_health_failure(sidecar_status: int) -> None:
|
||||
"""Ключевое значение issue: на бан-странице mark_health НЕ вызывается вовсе.
|
||||
|
||||
Именно этот вызов гнал узел к MAX_CONSECUTIVE_FAILS и выбивал его из выдачи
|
||||
ВСЕМ источникам (замер 01.09: узлы 9, 13, 14 на потолке при banned_for_source=0).
|
||||
"""
|
||||
pool = _FakePool()
|
||||
bf = await _fetcher(_ban_page_response(), pool)
|
||||
bf = await _fetcher(_ban_page_response(sidecar_status), pool)
|
||||
|
||||
with patch("scraper_kit.browser_fetcher.asyncio.sleep", AsyncMock()):
|
||||
with pytest.raises(AvitoBlockedError):
|
||||
|
|
@ -324,7 +342,8 @@ async def test_rotation_after_ban_does_not_ban_the_fresh_node() -> None:
|
|||
# ── (е) одна бан-страница — один POST ─────────────────────────────────────────
|
||||
|
||||
|
||||
async def test_ban_page_costs_exactly_one_post() -> None:
|
||||
@pytest.mark.parametrize("sidecar_status", _SIDECAR_BAN_CODES)
|
||||
async def test_ban_page_costs_exactly_one_post(sidecar_status: int) -> None:
|
||||
"""Ретрай `fetch()` не имеет права трогать бан-страницу: это ответ площадки.
|
||||
|
||||
Фальсификация: убери `except SidecarBanPageError: raise` перед
|
||||
|
|
@ -332,7 +351,7 @@ async def test_ban_page_costs_exactly_one_post() -> None:
|
|||
`_lease_fail_streak` вместо +1 (ротация вдвое раньше задуманного).
|
||||
"""
|
||||
pool = _FakePool()
|
||||
bf = await _fetcher(_ban_page_response(), pool)
|
||||
bf = await _fetcher(_ban_page_response(sidecar_status), pool)
|
||||
|
||||
with patch("scraper_kit.browser_fetcher.asyncio.sleep", AsyncMock()):
|
||||
with pytest.raises(AvitoBlockedError):
|
||||
|
|
|
|||
|
|
@ -175,9 +175,10 @@ def test_ban_kind_values_fit_scrape_runs_check() -> None:
|
|||
# 'platform', то есть ротация IP не запустилась бы вовсе.
|
||||
|
||||
|
||||
def _sidecar_response(body: Any) -> httpx.Response:
|
||||
def _sidecar_response(body: Any, status: int = 403) -> httpx.Response:
|
||||
"""Ответ сайдкара. 403 — код подтверждённого бана с #3288 п.4 (был 500)."""
|
||||
return httpx.Response(
|
||||
500, json=body, request=httpx.Request("POST", "http://tradein-browser:3000/fetch")
|
||||
status, json=body, request=httpx.Request("POST", "http://tradein-browser:3000/fetch")
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -190,6 +191,48 @@ def test_ban_page_body_raises_typed_error_with_upstream_status() -> None:
|
|||
assert excinfo.value.upstream_status == 401
|
||||
|
||||
|
||||
def test_ban_page_legacy_500_still_recognised() -> None:
|
||||
"""Сайдкар старого образа отдаёт тот же ban_page с кодом 500 — диагноз тот же.
|
||||
|
||||
tradein-browser собирается ОТДЕЛЬНЫМ образом и деплоится своим шагом, так что
|
||||
после выката #3288 клиент часами работает со старым сайдкаром. Гейта по коду
|
||||
нет намеренно: он бы в этот час уводил отказ площадки в инфра-ветку.
|
||||
"""
|
||||
resp = _sidecar_response(
|
||||
{"error": "BanPageDetectedError: статический отказ", "ban_page": True, "status": 401},
|
||||
status=500,
|
||||
)
|
||||
with pytest.raises(SidecarBanPageError) as excinfo:
|
||||
_raise_for_sidecar_status(resp)
|
||||
assert excinfo.value.upstream_status == 401
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sidecar_status", [403, 500])
|
||||
def test_ban_page_message_has_no_httpx_preamble(sidecar_status: int) -> None:
|
||||
"""Текст ошибки называет виновника: отказ ПЛОЩАДКИ, а не падение нашего сайдкара.
|
||||
|
||||
До #3288 п.4 текст начинался httpx-преамбулой «Server error '500 Internal Server
|
||||
Error' for url 'http://tradein-browser:3000/fetch'» — и ровно она уезжала в лог
|
||||
провайдера, в сообщение AvitoBlockedError/DomClickBlockedError и оттуда в
|
||||
`scrape_runs.error` (mark_failed), отправляя разбор по ложному следу.
|
||||
|
||||
Кусаемся за «for url», а НЕ за «500»/«Server error»: у 403-го ответа преамбула
|
||||
звучит «Client error '403 Forbidden' for url …», так что оба прежних утверждения
|
||||
были зелёными и на старом коде — проверка не могла упасть. «for url» httpx
|
||||
печатает при ЛЮБОМ коде, поэтому она кусается на обоих.
|
||||
"""
|
||||
resp = _sidecar_response(
|
||||
{"error": "BanPageDetectedError: доступ ограничен", "ban_page": True, "status": 403},
|
||||
status=sidecar_status,
|
||||
)
|
||||
with pytest.raises(SidecarBanPageError) as excinfo:
|
||||
_raise_for_sidecar_status(resp)
|
||||
message = str(excinfo.value)
|
||||
assert "for url" not in message, f"httpx-преамбула вернулась в текст: {message}"
|
||||
assert "бан-страницу" in message
|
||||
assert "upstream 403" in message
|
||||
|
||||
|
||||
def test_ban_page_error_is_httpx_status_error() -> None:
|
||||
"""Подкласс — иначе retry-политика fetch() и ловля у прочих поставщиков сломались бы."""
|
||||
resp = _sidecar_response({"error": "BanPageDetectedError: x", "ban_page": True, "status": 403})
|
||||
|
|
@ -198,7 +241,8 @@ def test_ban_page_error_is_httpx_status_error() -> None:
|
|||
|
||||
|
||||
def test_plain_500_stays_plain_status_error() -> None:
|
||||
resp = _sidecar_response({"error": "Error: Page.goto: NS_ERROR_PROXY_BAD_GATEWAY"})
|
||||
"""Настоящий сбой сайдкара остаётся 500-кой БЕЗ ban_page → инфра-диагноз, как раньше."""
|
||||
resp = _sidecar_response({"error": "Error: Page.goto: NS_ERROR_PROXY_BAD_GATEWAY"}, status=500)
|
||||
with pytest.raises(httpx.HTTPStatusError) as excinfo:
|
||||
_raise_for_sidecar_status(resp)
|
||||
assert not isinstance(excinfo.value, SidecarBanPageError)
|
||||
|
|
@ -230,7 +274,8 @@ def test_non_json_error_body_does_not_break() -> None:
|
|||
|
||||
def test_success_response_raises_nothing() -> None:
|
||||
resp = httpx.Response(
|
||||
200, json={"html": "<html/>", "status": 200},
|
||||
200,
|
||||
json={"html": "<html/>", "status": 200},
|
||||
request=httpx.Request("POST", "http://tradein-browser:3000/fetch"),
|
||||
)
|
||||
_raise_for_sidecar_status(resp)
|
||||
|
|
|
|||
|
|
@ -1466,10 +1466,23 @@ async def fetch_handler(request: web.Request) -> web.Response:
|
|||
# транспорта, и только за первым стоит report_ban на клиенте.
|
||||
# До этой правки оба случая приезжали одинаковой 500-кой, клиент
|
||||
# различить их не мог и настоящий отказ площадки переставал
|
||||
# ротировать узел (регрессия #3237). Код ответа НЕ меняем: на 500
|
||||
# завязана classify_browser_probe, признак несёт тело.
|
||||
# ротировать узел (регрессия #3237).
|
||||
#
|
||||
# #3288 п.4: код ответа теперь РАЗНЫЙ. 500 означало и «площадка
|
||||
# забанила», и «сайдкар упал», поэтому разбор каждого инцидента
|
||||
# начинался с ложного следа («Server error 500 for url
|
||||
# tradein-browser:3000/fetch» в логе провайдера). 403 —
|
||||
# «в доступе отказано», ровно то, что произошло НА ПЛОЩАДКЕ;
|
||||
# 451 семантически про юридическую блокировку, это не она.
|
||||
# Своих 403 сайдкар не отдаёт (auth/валидация — 400/422/503),
|
||||
# так что код однозначен. classify_browser_probe не задет: у неё
|
||||
# любой status >= 400 → "sidecar", 403 и 500 там неразличимы.
|
||||
# Признак ban_page в теле ОСТАЁТСЯ — клиент опознаёт бан по нему,
|
||||
# а не по коду (сайдкар и backend — разные образы, версии едут
|
||||
# врозь).
|
||||
error_body["ban_page"] = True
|
||||
error_body["status"] = _last_response_status.get(provider)
|
||||
return web.json_response(error_body, status=403)
|
||||
return web.json_response(error_body, status=500)
|
||||
|
||||
# Аддитивно (#3196): ключ "html" на месте и не изменился — клиент, читающий
|
||||
|
|
|
|||
|
|
@ -354,16 +354,20 @@ def _fetch_handler_error_body(
|
|||
|
||||
|
||||
def test_fetch_handler_marks_ban_page_in_error_body(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Бан-страница несёт ban_page + апстрим-статус.
|
||||
"""Бан-страница несёт ban_page + апстрим-статус И ОТЛИЧИМЫЙ КОД 403.
|
||||
|
||||
Без этого признака клиент видит обычную 500-ку, уводит отказ в транспортную
|
||||
Без признака в теле клиент видит обычную 500-ку, уводит отказ в транспортную
|
||||
ветку (report_ban там не зовётся) и перестаёт ротировать отказавший узел —
|
||||
регрессия, которую #3237 внёс, а #3239 чинит.
|
||||
|
||||
Код 403 (#3288 п.4): один и тот же 500 означал «площадка забанила» и
|
||||
«сайдкар упал», и разбор инцидента начинался с ложного следа. Тело не
|
||||
меняется — клиент опознаёт бан по нему (образы деплоятся врозь).
|
||||
"""
|
||||
status, body = _fetch_handler_error_body(
|
||||
monkeypatch, server.BanPageDetectedError("статический отказ площадки"), 401
|
||||
)
|
||||
assert status == 500 # код НЕ меняем: на него завязана classify_browser_probe
|
||||
assert status == 403
|
||||
assert body["ban_page"] is True
|
||||
assert body["status"] == 401
|
||||
assert "BanPageDetectedError" in body["error"]
|
||||
|
|
|
|||
|
|
@ -164,6 +164,16 @@ def _sidecar_ban_page_status(resp: httpx.Response) -> tuple[bool, int | None]:
|
|||
Тело — ``{"error": ..., "ban_page": true, "status": <int|null>}``. Сайдкар
|
||||
старой сборки ключей не отдаёт → (False, None), поведение как до правки.
|
||||
``bool`` отсекаем явно: он подтип ``int`` и JSON ``true`` уехал бы статусом.
|
||||
|
||||
Признак — ТЕЛО, а не код ответа, и проверка намеренно остаётся
|
||||
code-agnostic: с #3288 сайдкар отвечает на бан 403, до него отвечал 500, а
|
||||
``tradein-browser`` — ОТДЕЛЬНЫЙ образ (``gendesign-tradein-browser``,
|
||||
docker-compose.prod.yml) со своим деплоем, т.е. версии сайдкара и backend
|
||||
штатно разъезжаются на несколько часов. Оба кода дают одинаковый диагноз,
|
||||
гейта по коду тут нет и заводить его не нужно: он ничего не добавит (свой
|
||||
``ban_page`` умеет писать только наш сайдкар), а в час рассинхрона снова
|
||||
уводил бы отказ площадки в инфра-ветку. Снимать «совместимость» поэтому
|
||||
нечего — 500-ветка исчезнет сама, когда сайдкар перестанет её отдавать.
|
||||
"""
|
||||
try:
|
||||
body = resp.json()
|
||||
|
|
@ -203,8 +213,28 @@ def _raise_for_sidecar_status(resp: httpx.Response) -> None:
|
|||
# #3239: тип несёт диагноз наверх — подстрокой в тексте его искать
|
||||
# нельзя, detail обрезан до 300 символов и формулировка отказа
|
||||
# менялась дважды за месяц.
|
||||
#
|
||||
# #3288 п.4: текст СВОЙ, без httpx-преамбулы `{exc}`. Она печатала
|
||||
# «Server error '500 Internal Server Error' for url
|
||||
# 'http://tradein-browser:3000/fetch'», и этой строкой отказ ПЛОЩАДКИ
|
||||
# читался как падение НАШЕГО сайдкара — разбор инцидента каждый раз
|
||||
# начинался с ложного следа («текст ошибки называет гонца»).
|
||||
# Куда текст реально доезжает: провайдер подставляет его в свой лог и
|
||||
# в сообщение доменного исключения (providers/avito/serp.py — logger
|
||||
# «avito page=%d sidecar ban page» + AvitoBlockedError,
|
||||
# providers/domclick/detail.py — DomClickBlockedError «...: {exc}»),
|
||||
# а задача кладёт `str(exc)[:1000]` в `scrape_runs.error` через
|
||||
# `mark_failed` (например domclick_detail_backfill.py). В
|
||||
# `houses.imv_error_reason` он НЕ попадает: IMV ходит только через
|
||||
# `/fetch-json`, а `fetch_json_handler` (browser/server.py) `ban_page`
|
||||
# в тело не пишет — SidecarBanPageError там не возникает вовсе.
|
||||
# Код ответа сайдкара оставляем в тексте отдельным числом: он
|
||||
# отличает 403 нового сайдкара от 500 старого, но больше ничего не
|
||||
# значит.
|
||||
raise SidecarBanPageError(
|
||||
f"{exc} | tradein-browser: {detail or 'ban page'}",
|
||||
f"tradein-browser: площадка отдала бан-страницу "
|
||||
f"(upstream {upstream_status if upstream_status is not None else '?'}, "
|
||||
f"ответ сайдкара {resp.status_code}): {detail or 'ban page'}",
|
||||
request=exc.request,
|
||||
response=exc.response,
|
||||
upstream_status=upstream_status,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue