diff --git a/tradein-mvp/backend/tests/test_3118_domclick_warm_context.py b/tradein-mvp/backend/tests/test_3118_domclick_warm_context.py
index fb96e3cc..09d77e7a 100644
--- a/tradein-mvp/backend/tests/test_3118_domclick_warm_context.py
+++ b/tradein-mvp/backend/tests/test_3118_domclick_warm_context.py
@@ -239,7 +239,9 @@ async def test_3118_pending_reset_consumed_exactly_once():
bf = _fetcher(reuse_context=True)
seen: list[bool] = []
- async def _fake_post(url, origin=None, cookies=None, reset_context=False, referer=None):
+ async def _fake_post(
+ url, origin=None, cookies=None, reset_context=False, referer=None, fetch_mode=None
+ ):
seen.append(reset_context)
return ""
@@ -261,7 +263,9 @@ async def test_3118_pending_reset_survives_transport_retry():
bf = _fetcher(reuse_context=True)
seen: list[bool] = []
- async def _fake_post(url, origin=None, cookies=None, reset_context=False, referer=None):
+ async def _fake_post(
+ url, origin=None, cookies=None, reset_context=False, referer=None, fetch_mode=None
+ ):
seen.append(reset_context)
if len(seen) == 1:
raise httpx.ConnectError("boom")
diff --git a/tradein-mvp/backend/tests/test_3264_sweep_subresource_mode.py b/tradein-mvp/backend/tests/test_3264_sweep_subresource_mode.py
new file mode 100644
index 00000000..89fff284
--- /dev/null
+++ b/tradein-mvp/backend/tests/test_3264_sweep_subresource_mode.py
@@ -0,0 +1,95 @@
+"""#3264: свип берёт BFF подзапросом из прогретой страницы, а не навигацией.
+
+BFF-ручка Домклика — не страница, а JSON-эндпоинт SPA. Навигацией браузера на неё
+через мобильные узлы пула приходит только ChallengeTimeout (прод, прогоны 5330 и 5351:
+9/9 и 6/6 запросов зависли на челлендже), а подзапросом из прогретого контекста —
+HTTP 200 за 6 секунд. Замер 30.08.2026: все четыре узла пула, все шесть комнатных
+корзин, суммарно 6359 офферов против 6367, снятых напрямую с резидентного IP.
+
+Тестируется контракт, а не обход защиты:
+ 1. оба вызова свипа идут с fetch_mode="subresource" И с непустым origin —
+ без прогрева рукопожатие в куки контекста не попадёт, режим бесполезен;
+ 2. BrowserFetcher кладёт fetch_mode в payload ТОЛЬКО когда он не дефолтный —
+ сайдкар прежней версии не должен получать незнакомый ключ.
+"""
+
+from __future__ import annotations
+
+import os
+from types import SimpleNamespace
+from unittest.mock import AsyncMock, MagicMock
+
+os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
+
+import pytest
+from scraper_kit.providers.domclick.serp import _SWEEP_ORIGIN, DomClickScraper
+
+
+def _scraper() -> DomClickScraper:
+ return DomClickScraper(
+ SimpleNamespace(scraper_fetch_mode="browser", browser_http_endpoint="http://b/fetch"),
+ cookies={"qrator_jsid2": "x"},
+ )
+
+
+@pytest.mark.asyncio
+async def test_count_probe_uses_subresource_with_warmed_origin() -> None:
+ s = _scraper()
+ fetcher = MagicMock()
+ fetcher.fetch = AsyncMock(return_value='{"result":{"offersCount":2202}}')
+ await s._count(fetcher, "2", None, None) # type: ignore[attr-defined]
+ kw = fetcher.fetch.await_args.kwargs
+ assert kw["fetch_mode"] == "subresource"
+ assert kw["origin"] == _SWEEP_ORIGIN and kw["origin"]
+ assert kw["cookies"] == {"qrator_jsid2": "x"}
+
+
+@pytest.mark.asyncio
+async def test_pagination_uses_subresource_with_warmed_origin() -> None:
+ """Второй вызов — листалка; режим и прогрев должны совпадать с count-пробой."""
+ s = _scraper()
+ fetcher = MagicMock()
+ fetcher.fetch = AsyncMock(return_value='{"result":{"items":[]}}')
+ await s._paginate(fetcher, "2", None, None, set(), [], 1) # type: ignore[attr-defined]
+ assert fetcher.fetch.await_count >= 1
+ kw = fetcher.fetch.await_args.kwargs
+ assert kw["fetch_mode"] == "subresource"
+ assert kw["origin"] == _SWEEP_ORIGIN
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+ ("mode", "ожидается_в_payload"),
+ [("subresource", True), ("navigate", False), (None, False)],
+)
+async def test_fetch_mode_reaches_payload_only_when_meaningful(
+ mode: str | None, ожидается_в_payload: bool
+) -> None:
+ """Дефолтный режим в payload не попадает: старый сайдкар не знает такого ключа."""
+ from scraper_kit.browser_fetcher import BrowserFetcher
+
+ bf = BrowserFetcher(source="domclick", endpoint="http://b/fetch")
+ bf._client = MagicMock()
+ captured: dict = {}
+
+ class _Resp:
+ status_code = 200
+
+ @staticmethod
+ def json() -> dict:
+ return {"html": "ok", "status": 200}
+
+ @staticmethod
+ def raise_for_status() -> None:
+ return None
+
+ async def _post(_url: str, json: dict, **_kw: object) -> _Resp:
+ captured.update(json)
+ return _Resp()
+
+ bf._client.post = _post
+ await bf._post_fetch("https://bff-search-web.domclick.ru/api/offers/v1",
+ _SWEEP_ORIGIN, {"c": "1"}, False, _SWEEP_ORIGIN, mode)
+ assert ("fetch_mode" in captured) is ожидается_в_payload
+ if ожидается_в_payload:
+ assert captured["fetch_mode"] == mode
diff --git a/tradein-mvp/browser/server.py b/tradein-mvp/browser/server.py
index d31dd8c5..c1fbf0ea 100644
--- a/tradein-mvp/browser/server.py
+++ b/tradein-mvp/browser/server.py
@@ -138,6 +138,7 @@ Per-provider модель (#1793):
import asyncio
import base64
+import gzip
import logging
import os
from collections.abc import Callable, Mapping
@@ -1259,6 +1260,16 @@ async def fetch_handler(request: web.Request) -> web.Response:
reuse_context: bool = bool(body.get("reuse_context", False))
reset_context: bool = bool(body.get("reset_context", False))
+ # fetch_mode (#3264) — чем забирать ресурс: навигацией (дефолт, прежнее поведение),
+ # запросом контекста или fetch'ем из прогретой страницы. См. FETCH_MODES.
+ fetch_mode: str = str(body.get("fetch_mode") or "navigate")
+ if fetch_mode not in FETCH_MODES:
+ return web.json_response(
+ {"error": f"неизвестный fetch_mode={fetch_mode!r}, "
+ f"допустимы {sorted(FETCH_MODES)}"},
+ status=400,
+ )
+
provider = _resolve_provider(body, url)
proxy_override = _resolve_proxy_override(body, provider)
@@ -1299,6 +1310,7 @@ async def fetch_handler(request: web.Request) -> web.Response:
cookies=cookies,
reuse_context=reuse_context,
reset_context=reset_context,
+ fetch_mode=fetch_mode,
)
# Читаем статус под тем же локом, что и сам фетч, — иначе следующий
# запрос этого провайдера успел бы его перезаписать.
@@ -1464,6 +1476,41 @@ async def _pace_provider(provider: str) -> None:
_last_goto_at[provider] = asyncio.get_event_loop().time()
+# ── Режимы получения тела (#3264) ────────────────────────────────────────────────
+# Сайдкар всю жизнь умел ровно одно: navigate — page.goto(url). Для HTML-страницы это
+# и есть то, что делает человек. Но два нужных нам ресурса Домклика страницами НЕ
+# являются: BFF-ручка /api/offers/v1 отдаёт JSON, а карта офферов
+# (sitemap-offers-1.xml.gz) — вообще gzip-файл. Живой сайт их так и не запрашивает:
+# перехват сети на выдаче 30.08.2026 показал, что офферы приезжают в SSR-документе, а
+# к BFF ходят XHR'ы за гео/районами/метро. Навигацией браузера на API-хост мы делаем
+# то, чего настоящий клиент не делает никогда, — и через узлы пула это упирается в
+# ChallengeTimeout, тогда как карточки через те же узлы в те же минуты идут.
+#
+# Отсюда два дополнительных режима, оба — ПОСЛЕ прогрева origin:
+# subresource — context.request.get(url): запрос из browser-контекста, куки и прокси
+# общие со страницей, но это не JS страницы;
+# page_fetch — fetch(url, {credentials: 'include'}) ИЗ прогретой страницы: ровно то,
+# что делает SPA, вместе с Origin/Referer/Sec-Fetch-* и cookie jar'ом.
+# Какой из них проходит через пул — вопрос замера, а не рассуждения, поэтому оба.
+FETCH_MODES: frozenset[str] = frozenset({"navigate", "subresource", "page_fetch"})
+
+
+def _decode_body(raw: bytes) -> str:
+ """Тело подзапроса строкой: gzip-файлы распаковываем, прочее декодируем как UTF-8.
+
+ Карта офферов приезжает с Content-Type: application/gzip — это gzip-ФАЙЛ, а не
+ Content-Encoding, поэтому HTTP-стек его не распаковывает и вызывающий получил бы
+ бинарь в поле "html". Распаковываем здесь по магическим байтам, чтобы контракт
+ /fetch остался прежним — «тело строкой».
+ """
+ if raw[:2] == bytes((0x1F, 0x8B)): # магия gzip
+ try:
+ raw = gzip.decompress(raw)
+ except Exception: # noqa: BLE001 — битый gzip отдаём как есть, пусть решает caller
+ pass
+ return raw.decode("utf-8", "replace")
+
+
async def _do_fetch(
provider: str,
url: str,
@@ -1473,6 +1520,7 @@ async def _do_fetch(
cookies: dict | None = None,
reuse_context: bool = False,
reset_context: bool = False,
+ fetch_mode: str = "navigate",
) -> str:
"""Одна попытка навигации; при краше браузера — relaunch и один retry.
@@ -1495,6 +1543,7 @@ async def _do_fetch(
cookies=cookies,
reuse_context=reuse_context,
reset_context=reset_context,
+ fetch_mode=fetch_mode,
)
except Exception as exc:
if _is_browser_crash(exc):
@@ -1515,6 +1564,7 @@ async def _do_fetch(
cookies=cookies,
reuse_context=reuse_context,
reset_context=reset_context,
+ fetch_mode=fetch_mode,
)
raise
@@ -1834,6 +1884,45 @@ async def _get_or_create_context(
return context
+async def _fetch_subresource(
+ page: object, url: str, fetch_mode: str, referer: str | None
+) -> tuple[bytes, int]:
+ """Забирает *url* подзапросом из прогретой страницы. Возвращает (тело, HTTP-статус).
+
+ Два режима, отличающихся тем, ЧЬИМ запросом идёт обращение:
+
+ ``subresource`` — ``context.request.get``: запрос браузерного контекста. Куки, прокси
+ и TLS общие со страницей, но это не JS страницы, поэтому заголовков ``Sec-Fetch-*`` и
+ ``Origin`` у него нет.
+
+ ``page_fetch`` — ``fetch()``, выполненный ВНУТРИ страницы. Ровно то, что делает SPA:
+ с ``Origin``/``Referer``/``Sec-Fetch-*`` и общим cookie jar'ом. Тело возвращается
+ через base64, потому что ``page.evaluate`` умеет отдавать только JSON-совместимое,
+ а карта офферов — двоичный gzip.
+ """
+ if fetch_mode == "subresource":
+ headers = {"Referer": referer} if referer else None
+ resp = await page.context.request.get( # type: ignore[attr-defined]
+ url, timeout=BROWSER_NAV_TIMEOUT_MS, headers=headers
+ )
+ return await resp.body(), int(resp.status)
+
+ if fetch_mode == "page_fetch":
+ res = await page.evaluate( # type: ignore[attr-defined]
+ """async (u) => {
+ const r = await fetch(u, {credentials: 'include'});
+ const buf = new Uint8Array(await r.arrayBuffer());
+ let s = '';
+ for (let i = 0; i < buf.length; i++) s += String.fromCharCode(buf[i]);
+ return {status: r.status, b64: btoa(s)};
+ }""",
+ url,
+ )
+ return base64.b64decode(res["b64"]), int(res["status"])
+
+ raise ValueError(f"неизвестный fetch_mode={fetch_mode!r}, допустимы {sorted(FETCH_MODES)}")
+
+
async def _fetch_once(
provider: str,
url: str,
@@ -1843,6 +1932,7 @@ async def _fetch_once(
cookies: dict | None = None,
reuse_context: bool = False,
reset_context: bool = False,
+ fetch_mode: str = "navigate",
) -> str:
"""Открывает страницу, переходит по URL, ждёт JS, возвращает HTML.
@@ -1929,6 +2019,31 @@ async def _fetch_once(
origin, timeout=BROWSER_NAV_TIMEOUT_MS, wait_until="domcontentloaded"
)
await page.wait_for_timeout(FETCH_JSON_SETTLE_MS) # type: ignore[attr-defined]
+ if fetch_mode != "navigate":
+ # Ресурс берём ПОДЗАПРОСОМ из уже прогретого контекста, а не навигацией
+ # (#3264). Прогрев origin выше обязателен: именно он проносит рукопожатие
+ # QRATOR в куки контекста, из которого пойдёт подзапрос.
+ body_bytes, status = await _fetch_subresource(page, url, fetch_mode, referer)
+ _last_response_status[provider] = status
+ text = _decode_body(body_bytes)
+ if _is_ban_page(text):
+ raise BanPageDetectedError(
+ f"tradein-browser[{provider}]: бан-страница (проблема с IP) url={url!r}"
+ )
+ if provider == "domclick" and _is_domclick_refusal(text):
+ raise BanPageDetectedError(
+ f"tradein-browser[{provider}]: статический отказ площадки url={url!r}"
+ )
+ logger.info(
+ "tradein-browser[%s]: %s → HTTP %s, тело %d Б url=%r",
+ provider,
+ fetch_mode,
+ status,
+ len(text),
+ url,
+ )
+ return text
+
# Статус берём у ЦЕЛЕВОЙ навигации, а не у прогрева origin выше: интересует
# ответ площадки на запрошенный url (#3196).
goto_kwargs: dict[str, object] = {
diff --git a/tradein-mvp/browser/test_server.py b/tradein-mvp/browser/test_server.py
index 226d3da5..046e6a2d 100644
--- a/tradein-mvp/browser/test_server.py
+++ b/tradein-mvp/browser/test_server.py
@@ -182,6 +182,7 @@ class _OverlapProbe:
cookies: dict | None = None,
reuse_context: bool = False,
reset_context: bool = False,
+ fetch_mode: str = "navigate",
) -> str:
self.active[provider] = self.active.get(provider, 0) + 1
self.peak[provider] = max(self.peak.get(provider, 0), self.active[provider])
diff --git a/tradein-mvp/browser/test_server_fetch_mode.py b/tradein-mvp/browser/test_server_fetch_mode.py
new file mode 100644
index 00000000..aec35c9a
--- /dev/null
+++ b/tradein-mvp/browser/test_server_fetch_mode.py
@@ -0,0 +1,112 @@
+"""test_server_fetch_mode.py — режимы получения тела: навигация против подзапроса (#3264).
+
+BFF-ручка Домклика и карта офферов страницами не являются. Навигацией браузера на
+BFF через мобильные узлы пула приходит только ChallengeTimeout, подзапросом из
+прогретого контекста — HTTP 200 за 6 секунд (замер 30.08.2026, четыре узла, шесть
+корзин). Здесь проверяется контракт режимов, а не поведение площадки.
+
+Тестируется:
+ 1. дефолт остаётся навигацией — goto зовётся, подзапрос нет;
+ 2. subresource НЕ навигирует на целевой url и берёт тело запросом контекста;
+ 3. gzip-файл распаковывается (карта офферов приезжает application/gzip);
+ 4. неизвестный режим отбивается 400, а не падает внутри.
+
+camoufox НЕ запускается — browser/context/page подделаны.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import gzip
+import importlib.util
+from pathlib import Path
+from typing import Any
+
+import pytest
+
+_SERVER_PATH = Path(__file__).resolve().parent / "server.py"
+_spec = importlib.util.spec_from_file_location("tradein_browser_server", _SERVER_PATH)
+assert _spec is not None and _spec.loader is not None
+server = importlib.util.module_from_spec(_spec)
+_spec.loader.exec_module(server)
+
+
+class _Resp:
+ def __init__(self, raw: bytes, status: int = 200) -> None:
+ self._raw, self.status = raw, status
+
+ async def body(self) -> bytes:
+ return self._raw
+
+
+class _Request:
+ def __init__(self, raw: bytes) -> None:
+ self._raw = raw
+ self.calls: list[tuple[str, Any]] = []
+
+ async def get(self, url: str, **kwargs: Any) -> _Resp:
+ self.calls.append((url, kwargs))
+ return _Resp(self._raw)
+
+
+class _Context:
+ def __init__(self, raw: bytes) -> None:
+ self.request = _Request(raw)
+
+
+class _Page:
+ def __init__(self, raw: bytes = b"{}") -> None:
+ self.context = _Context(raw)
+ self.goto_urls: list[str] = []
+ self.evaluated: list[str] = []
+
+ async def goto(self, url: str, **_kw: Any) -> None:
+ self.goto_urls.append(url)
+
+ async def evaluate(self, _script: str, url: str) -> dict:
+ self.evaluated.append(url)
+ return {"status": 200, "b64": ""}
+
+
+def test_gzip_body_is_decompressed() -> None:
+ """Карта офферов приезжает Content-Type: application/gzip — стек её не распакует."""
+ raw = gzip.compress("x".encode())
+ assert server._decode_body(raw) == "x"
+
+
+def test_plain_body_passes_through() -> None:
+ assert server._decode_body(b'{"result":{"offersCount":2202}}') == (
+ '{"result":{"offersCount":2202}}'
+ )
+
+
+def test_subresource_does_not_navigate_to_target() -> None:
+ """Смысл режима: на целевой url НЕ ходим навигацией, берём его подзапросом."""
+ page = _Page(b'{"result":{"offersCount":2202}}')
+ raw, status = asyncio.run(
+ server._fetch_subresource(page, "https://bff-search-web.domclick.ru/api/x",
+ "subresource", "https://ekaterinburg.domclick.ru/serp")
+ )
+ assert page.goto_urls == []
+ assert status == 200
+ assert b"offersCount" in raw
+ url, kwargs = page.context.request.calls[0]
+ assert url == "https://bff-search-web.domclick.ru/api/x"
+ assert kwargs["headers"] == {"Referer": "https://ekaterinburg.domclick.ru/serp"}
+
+
+def test_subresource_without_referer_sends_no_header() -> None:
+ page = _Page()
+ asyncio.run(server._fetch_subresource(page, "https://x.domclick.ru/api", "subresource", None))
+ assert page.context.request.calls[0][1]["headers"] is None
+
+
+def test_unknown_mode_raises_named_error() -> None:
+ page = _Page()
+ with pytest.raises(ValueError, match="fetch_mode"):
+ asyncio.run(server._fetch_subresource(page, "https://x/api", "телепортация", None))
+
+
+def test_navigate_is_the_default_and_stays_a_valid_mode() -> None:
+ assert "navigate" in server.FETCH_MODES
+ assert {"navigate", "subresource", "page_fetch"} == set(server.FETCH_MODES)
diff --git a/tradein-mvp/packages/scraper-kit/src/scraper_kit/browser_fetcher.py b/tradein-mvp/packages/scraper-kit/src/scraper_kit/browser_fetcher.py
index 9476e72f..1aa996e1 100644
--- a/tradein-mvp/packages/scraper-kit/src/scraper_kit/browser_fetcher.py
+++ b/tradein-mvp/packages/scraper-kit/src/scraper_kit/browser_fetcher.py
@@ -468,9 +468,19 @@ class BrowserFetcher:
referer: str | None = None,
cookies: dict[str, str] | None = None,
reset_context: bool = False,
+ fetch_mode: str | None = None,
) -> str:
"""Запрашивает HTML страницы через tradein-browser HTTP-сервис.
+ fetch_mode (#3264) — чем сайдкар заберёт ресурс: ``None``/``"navigate"`` —
+ обычная навигация браузера (дефолт, прежнее поведение), ``"subresource"`` —
+ запрос из прогретого browser-контекста. Второй режим нужен ресурсам, которые
+ страницами не являются: BFF-ручка Домклика отдаёт JSON, и навигацией браузера
+ на неё через мобильные прокси приходит только ChallengeTimeout, тогда как
+ подзапросом — HTTP 200 за 6 секунд (замер 30.08.2026, все 4 узла пула, все 6
+ комнатных корзин). Требует непустого origin: рукопожатие QRATOR попадает в
+ куки контекста именно при прогреве.
+
origin — same-site якорь, на который камуфокс зайдёт ПЕРЕД url (прогревает
cookies контекста ДО холодного goto на url), см. /fetch ``origin`` в
server.py. None (дефолт) → поведение не меняется (ровно один goto(url), как
@@ -519,7 +529,9 @@ class BrowserFetcher:
self._context_reset_pending = False
try:
- return await self._post_fetch(url, origin, cookies, effective_reset, referer)
+ return await self._post_fetch(
+ url, origin, cookies, effective_reset, referer, fetch_mode=fetch_mode
+ )
except (httpx.HTTPError, httpx.TransportError) as exc:
logger.warning(
"BrowserFetcher: ошибка запроса (%s), retry через %.1fs: %s",
@@ -528,7 +540,9 @@ class BrowserFetcher:
url,
)
await asyncio.sleep(_RETRY_SLEEP_S)
- return await self._post_fetch(url, origin, cookies, effective_reset, referer)
+ return await self._post_fetch(
+ url, origin, cookies, effective_reset, referer, fetch_mode=fetch_mode
+ )
async def fetch_json(
self,
@@ -798,6 +812,7 @@ class BrowserFetcher:
cookies: dict[str, str] | None = None,
reset_context: bool = False,
referer: str | None = None,
+ fetch_mode: str | None = None,
) -> str:
"""Один HTTP POST к /fetch эндпоинту сервиса.
@@ -838,6 +853,11 @@ class BrowserFetcher:
payload["reset_context"] = reset_context
if referer:
payload["referer"] = referer
+ # fetch_mode кладём ТОЛЬКО когда задан и не дефолтный: сайдкар старой версии
+ # (без этого поля в схеме) не должен получать неожиданный ключ — тот же приём,
+ # что с referer выше.
+ if fetch_mode and fetch_mode != "navigate":
+ payload["fetch_mode"] = fetch_mode
if proxy_url:
payload["proxy"] = proxy_url
if proxy_kind:
diff --git a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py
index 6df94ed9..ac458a2a 100644
--- a/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py
+++ b/tradein-mvp/packages/scraper-kit/src/scraper_kit/providers/domclick/serp.py
@@ -158,6 +158,16 @@ def _build_offers_url(
return f"{_BFF_BASE}/api/offers/v1?{urlencode(params)}"
+# Страница, на которой прогревается контекст перед запросом к BFF (#3264).
+# BFF — не страница, а JSON-ручка SPA; навигацией браузера на неё через мобильные узлы
+# пула приходит только ChallengeTimeout (прод, прогоны 5330 и 5351 — 9/9 и 6/6 запросов
+# зависли), а подзапросом из прогретой выдачи — HTTP 200 за 6 секунд на всех четырёх
+# узлах и всех шести комнатных корзинах (замер 30.08.2026, суммарно 6359 офферов против
+# 6367, снятых напрямую с резидентного IP). Прогрев обязателен: рукопожатие QRATOR
+# попадает в куки контекста именно при заходе на страницу.
+_SWEEP_ORIGIN = "https://ekaterinburg.domclick.ru/pokupka/kvartiry/vtorichka"
+
+
def _build_count_url(
rooms: str,
price_gte: int | None,
@@ -482,7 +492,13 @@ class DomClickScraper(BaseScraper):
"""
url = _build_count_url(rooms, price_gte, price_lte)
logger.debug("domklik: count url=%s", url)
- html = await fetcher.fetch(url, cookies=self._cookies) # #3264: снимок Sber ID сессии, обходит QRATOR PoW
+ html = await fetcher.fetch(
+ url,
+ origin=_SWEEP_ORIGIN,
+ referer=_SWEEP_ORIGIN,
+ cookies=self._cookies,
+ fetch_mode="subresource",
+ )
try:
data = _extract_json(html)
except DomClickBlockedError:
@@ -571,7 +587,13 @@ class DomClickScraper(BaseScraper):
break
url = _build_offers_url(rooms, price_gte, price_lte, offset)
logger.debug("domklik: offers url=%s", url)
- html = await fetcher.fetch(url, cookies=self._cookies) # #3264: снимок Sber ID сессии, обходит QRATOR PoW
+ html = await fetcher.fetch(
+ url,
+ origin=_SWEEP_ORIGIN,
+ referer=_SWEEP_ORIGIN,
+ cookies=self._cookies,
+ fetch_mode="subresource",
+ )
try:
data = _extract_json(html)
except DomClickBlockedError: