fix(tradein/browser): снятый PoW-челлендж отдавался с протухшим статусом, и карточка выбрасывалась (#3283) (#3306)
All checks were successful
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Successful in 2m10s
Deploy Trade-In / test (push) Successful in 4m5s
Deploy Trade-In / build-backend (push) Successful in 1m39s
Deploy Trade-In / deploy (push) Successful in 8m32s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s

Co-authored-by: lekss361 <lekss361@gendsgn.local>
Co-committed-by: lekss361 <lekss361@gendsgn.local>
This commit is contained in:
lekss361 2026-08-31 14:54:00 +00:00 committed by bot-backend
parent 4c29b59041
commit 9cbdc5dfbe
2 changed files with 59 additions and 0 deletions

View file

@ -1783,6 +1783,10 @@ async def _wait_out_pow_challenge(
После снятия челленджа даём странице догидрироваться тем же BROWSER_WAIT_MS,
каким ждём обычную навигацию (второй таймаут не изобретаем).
ПОБОЧНЫЙ ЭФФЕКТ: на успешном выходе сбрасывает `_last_response_status[provider]`
в None статус challenge-ответа к этому моменту протух и не описывает
устоявшуюся страницу (обоснование в комментарии у самой строки, #3283).
"""
poll_interval_ms = 1000
elapsed_ms = 0
@ -1851,6 +1855,24 @@ async def _wait_out_pow_challenge(
f"tradein-browser[{provider}]: челлендж снят, но страница не устоялась "
f"(навигация не прекращается) url={url!r}"
)
# #3283: _last_response_status[provider] на этот момент всё ещё хранит статус
# ОТВЕТА НА CHALLENGE-СТРАНИЦУ (goto/подзапрос ДО вызова этой функции, напр.
# 439/401) — сама PoW-страница чинит себя self-reload'ом (`window.location =
# location.href`), Response на который мы не наблюдаем (опрашиваем content(),
# не подписаны на page.on("response")), а наш собственный page.reload() в цикле
# выше (после BROWSER_CHALLENGE_RELOAD_AFTER_MS) — лишь один из нескольких
# возможных источников устоявшегося content(), тоже не гарантированно последний.
# Замер прода (прогон 5606): рукопожатие снималось за 1-9с — БЫСТРЕЕ, чем успевал
# сработать наш reload (порог 8с) — то есть в типичном случае достоверного
# ответа на устоявшийся контент у нас нет вовсе. Отдать наверх протухший статус
# challenge-страницы как статус УСТОЯВШЕГОСЯ content() — воскресить #3283:
# avito/detail.py трактует 403/439 как блок площадки и выбрасывает уже
# вытащенную карточку. None — «статус неизвестен», клиент это НЕ считает блоком
# (см. providers/avito/detail.py: `if status is not None: ...`) и падает на
# html-эвристики, которые на настоящем контенте не сработают. Действует и на
# DomClick-ветку (is_pending=... выше) — там статус на успешном пути и раньше
# не проверялся при классификации блока, только логировался как диагностика.
_last_response_status[provider] = None
return settled

View file

@ -241,6 +241,43 @@ def test_fetch_once_still_waits_out_challenge_on_200(monkeypatch: pytest.MonkeyP
assert page.wait_for_timeout_calls # ожидание челленджа реально шло
def test_fetch_once_clears_stale_challenge_status_once_settled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""#3283: снятый челлендж не должен отдать наверх статус challenge-страницы.
Замер прода (прогон 5606): сайдкар пишет `_last_response_status` ОДИН раз, из
ответа на challenge-страницу (тут 439), и никогда его не обновляет после
self-reload'а, которым PoW чинит себя сам. Клиент (avito/detail.py) трактует
403/439 как блок площадки НЕ глядя в HTML и выбрасывает уже вытащенную карточку.
После этой правки статус обязан стать None («неизвестен») клиент такой статус
блоком не считает.
"""
page = _Page([_CHALLENGE_HTML, _REAL_HTML], [_Response(439)])
_install(monkeypatch, page)
html = asyncio.run(server._fetch_once("avito", "https://www.avito.ru/x"))
assert html == _REAL_HTML
assert server._last_response_status["avito"] is None
def test_fetch_once_challenge_timeout_keeps_status_as_is(monkeypatch: pytest.MonkeyPatch) -> None:
"""Челлендж НЕ снялся → ChallengeTimeoutError, статус ответа НЕ обнуляем.
Отказ остаётся отказом (требование задачи #3283): return settled — единственное
место, где мы решаем, что status протух; на ветке таймаута до него не доходит.
"""
monkeypatch.setattr(server, "BROWSER_CHALLENGE_WAIT_MS", 1000)
page = _Page([_CHALLENGE_HTML], [_Response(439)]) # челлендж висит вечно
_install(monkeypatch, page)
with pytest.raises(server.ChallengeTimeoutError):
asyncio.run(server._fetch_once("avito", "https://www.avito.ru/x"))
assert server._last_response_status["avito"] == 439
def test_fetch_once_ban_marker_still_raises_on_200(monkeypatch: pytest.MonkeyPatch) -> None:
"""Ветка _is_ban_page осталась: «проблема с IP» с кодом 200 — по-прежнему бан."""
page = _Page(["<html>Доступ ограничен: проблема с IP</html>"], [_Response(200)])