fix(sf): User-Agent header for okn-mk searchMap (bypass 403) (#1146)
All checks were successful
Deploy / changes (push) Successful in 6s
Deploy / build-backend (push) Successful in 1m14s
Deploy / build-frontend (push) Has been skipped
Deploy / build-worker (push) Successful in 2m30s
Deploy / deploy (push) Successful in 1m10s

Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
This commit is contained in:
bot-backend 2026-06-07 13:57:34 +00:00 committed by bot-reviewer
parent 396db20c48
commit 9ee35bebf0
2 changed files with 58 additions and 3 deletions

View file

@ -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:

View file

@ -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)