fix(tradein/browser): снятый PoW-челлендж отдавался с протухшим статусом, и карточка выбрасывалась (#3283)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 11s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / browser-tests (pull_request) Successful in 1m27s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 11s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
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 / browser-tests (pull_request) Successful in 1m27s
Сайдкар уже пережидает челлендж QRATOR и уже его снимает, но
_last_response_status[provider] остаётся статусом challenge-страницы: он
пишется один раз, из ответа первого goto, а _wait_out_pow_challenge его не
обновляет. Наружу уходило {"html": <нормальная карточка>, "status": 439}, и
клиент avito/detail.py браковал её по одному коду, не заглянув в HTML.
Прод, прогон 5606 — одни и те же объявления:
13:15:23 сайдкар: PoW снят за ~9000мс, догидрация ...8242031370
13:15:28 скрапер: BLOCKED #1 HTTP 439 ...8242031370
13:22:33 сайдкар: PoW снят за ~3000мс, догидрация ...8272722582
13:22:37 скрапер: BLOCKED #11 HTTP 439 ...8272722582
Минимум 3 блока из 7 в прогоне — выброшенные страницы, уже вытащенные.
Ловить «свежий» статус нечем: PoW-страница чинит себя своим
window.location-редиректом, Response на который мы не наблюдаем, а
собственный page.reload() включается только после 8с — при снятии за 1-9с он
обычно не срабатывает вовсе. Поэтому на успешном выходе статус сбрасывается в
None («неизвестен»), а не тащится протухшим. Клиент None блоком не считает
(detail.py: `if status is not None`) и падает на html-эвристики, которые на
настоящем контенте не срабатывают.
Правка в общей функции, поэтому чинит и ветку DomClick. Провал ожидания
(ChallengeTimeoutError) остаётся отказом: сброс стоит после всех raise.
Клиента править не потребовалось — test_3044_avito_439_classified_as_block
зелёный без изменений.
Тесты сайдкара: 219 passed.
This commit is contained in:
parent
9219ab6507
commit
67a136c95c
2 changed files with 59 additions and 0 deletions
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)])
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue