From 8b7f2ff247d83f06d878c87d4c895ea4a9ff5537 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 7 Jun 2026 16:53:07 +0300 Subject: [PATCH] =?UTF-8?q?fix(sf):=20=D0=B1=D1=80=D0=B0=D1=83=D0=B7=D0=B5?= =?UTF-8?q?=D1=80=D0=BD=D1=8B=D0=B9=20User-Agent=20=D0=B4=D0=BB=D1=8F=20ok?= =?UTF-8?q?n-mk=20searchMap=20(=D0=BE=D0=B1=D1=85=D0=BE=D0=B4=20403)=20(#1?= =?UTF-8?q?066)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/services/scrapers/okn_egrkn_client.py | 16 ++++++- .../scrapers/test_okn_egrkn_client.py | 45 ++++++++++++++++++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/backend/app/services/scrapers/okn_egrkn_client.py b/backend/app/services/scrapers/okn_egrkn_client.py index c61f36a8..f785d1b5 100644 --- a/backend/app/services/scrapers/okn_egrkn_client.py +++ b/backend/app/services/scrapers/okn_egrkn_client.py @@ -28,6 +28,19 @@ _SEARCHMAP_URL = "https://okn-mk.mkrf.ru/Maps/searchMap" # Таймаут на один HTTP-запрос к ЕГРКН (секунды) _REQUEST_TIMEOUT = 30.0 +# Browser headers -- required: without User-Agent the server returns 403. +# Content-Type intentionally omitted: httpx sets it automatically with data= +# (application/x-www-form-urlencoded); duplicating it would conflict. +_HEADERS: dict[str, str] = { + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" + ), + "X-Requested-With": "XMLHttpRequest", + "Referer": "https://okn-mk.mkrf.ru/Maps", + "Origin": "https://okn-mk.mkrf.ru", +} + def _build_form_data(address: str, category_type: int) -> list[tuple[str, str]]: """Построить form-data в формате jQuery serializeArray для поиска по карте. @@ -88,11 +101,10 @@ def _do_request( ) -> list[dict[str, Any]]: """Выполнить HTTP POST к searchMap. При SSL-ошибке с verify=True повторяет с verify=False.""" try: - with httpx.Client(timeout=_REQUEST_TIMEOUT, verify=verify) as client: + with httpx.Client(timeout=_REQUEST_TIMEOUT, verify=verify, headers=_HEADERS) as client: resp = client.post( _SEARCHMAP_URL, data=form_data, - headers={"Content-Type": "application/x-www-form-urlencoded"}, ) resp.raise_for_status() except httpx.HTTPStatusError as exc: diff --git a/backend/tests/services/scrapers/test_okn_egrkn_client.py b/backend/tests/services/scrapers/test_okn_egrkn_client.py index 905fec8f..43f5aa1b 100644 --- a/backend/tests/services/scrapers/test_okn_egrkn_client.py +++ b/backend/tests/services/scrapers/test_okn_egrkn_client.py @@ -12,7 +12,11 @@ from __future__ import annotations from typing import Any from unittest.mock import MagicMock, patch -from app.services.scrapers.okn_egrkn_client import _build_form_data, fetch_okn_points +from app.services.scrapers.okn_egrkn_client import ( + _HEADERS, + _build_form_data, + fetch_okn_points, +) # ── Вспомогательные функции ─────────────────────────────────────────────────── @@ -226,6 +230,45 @@ def test_build_form_data_structure() -> None: assert ("data[1][value]", "1") in result +# -- User-Agent header (bypass 403) ------------------------------------------ + + +def test_request_sends_browser_user_agent() -> None: + """httpx.Client is created with browser User-Agent from _HEADERS (bypass 403). + + Without UA, okn-mk.mkrf.ru returns 403. + With Chrome/125 UA: 200 response, cob_address=Ekaterinburg = 272 features. + Checks: + 1. _HEADERS constant contains User-Agent with Mozilla string. + 2. httpx.Client is called with headers=_HEADERS when fetch_okn_points runs. + """ + assert "User-Agent" in _HEADERS + assert "Mozilla" in _HEADERS["User-Agent"] + assert "X-Requested-With" in _HEADERS + assert "Referer" in _HEADERS + assert "Origin" in _HEADERS + + features = [_make_feature("ua-001", 56.83, 60.61)] + payload = _make_geojson_response(features) + captured_kwargs: list[dict] = [] + + def _make_client(**kwargs: Any) -> MagicMock: + captured_kwargs.append(dict(kwargs)) + mc = MagicMock() + mc.__enter__ = MagicMock(return_value=mc) + mc.__exit__ = MagicMock(return_value=False) + mc.post.return_value = _mock_response(payload) + return mc + + with patch("app.services.scrapers.okn_egrkn_client.httpx.Client", side_effect=_make_client): + fetch_okn_points("Ekaterinburg", 1) + + assert len(captured_kwargs) == 1 + sent_headers = captured_kwargs[0].get("headers", {}) + assert "User-Agent" in sent_headers + assert "Mozilla" in sent_headers["User-Agent"] + + def test_build_form_data_category_type_as_string() -> None: """category_type конвертируется в строку (httpx data= ожидает str).""" result = _build_form_data("Свердловская", 4) -- 2.45.3