zapret-kvn/tests/test_browser_fetch.py
loop-uh 02624f82bc
Some checks failed
Windows project source guards / test (push) Failing after 46s
browser-fetch: без тел запросов — путь загрузки в cronet-go падает SIGSEGV
Сеанс zapret-kvn-33 поймал на Android редкий SIGSEGV (addr 0) на первом
запросе движка; первым шёл DoH POST. Воспроизведено на Linux: стресс 200
POST с частым GC падает в каждом из 10 прогонов в bodyUploadProvider.Read
→ UploadDataSink.OnReadSucceeded (cronet-go, purego); тот же стресс GET —
2000/2000 без сбоя.

- browserfetch.Fetch отклоняет запросы с телом понятной ошибкой.
- Один Executor на время жизни движка: RoundTripper без своего вешал
  финализатор, уничтожавший исполнитель при сборке мусора.
- DoH Windows через браузерный стек — GET ?dns= (RFC 8484), как у Chrome
  для шаблона dns.google{?dns}. Затронут выпущенный 0.8.37: там DoH шёл
  POST, при падении помощник перезапускался, запрос — к следующему резолверу.

Проверено: win10 — 200/200 DoH за 2,6 с в 8 потоков, один процесс-помощник
без перезапусков, запасной путь Python не понадобился; цепочка Android
0001–0006 + этот 0007 + 0008 ложится на чистую базу.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 00:07:23 +03:00

79 lines
3.1 KiB
Python

"""Клиент браузерного стека (xray_fluent/network/browser_fetch.py) на подделке процесса."""
from __future__ import annotations
import json
from pathlib import Path
import sys
import threading
import unittest
from xray_fluent.network.browser_fetch import (
BrowserFetchError,
BrowserFetchService,
BrowserFetchUnavailable,
)
FAKE = Path(__file__).resolve().parent / "fixtures" / "fake_browser_fetch.py"
def _service() -> BrowserFetchService:
return BrowserFetchService(
executable=lambda: FAKE, launcher=lambda exe: [sys.executable, "-I", str(exe)], enabled=True,
)
class BrowserFetchServiceTests(unittest.TestCase):
def setUp(self) -> None:
self.service = _service()
self.addCleanup(self.service.close)
def test_request_round_trip_keeps_method_headers_and_body(self) -> None:
response = self.service.fetch(
"https://8.8.8.8/dns-query?dns=AAE", method="GET",
headers=(("accept", "application/dns-message"),), timeout=5,
)
self.assertEqual(response.status, 200)
self.assertEqual(response.header("content-type"), "application/json")
echo = json.loads(response.body)
self.assertEqual(echo["method"], "GET")
self.assertEqual(echo["headers"], [["accept", "application/dns-message"]])
self.assertEqual(echo["body"], "") # тела браузерный стек не принимает
def test_parallel_requests_share_one_process(self) -> None:
results: list[int] = []
def run() -> None:
results.append(self.service.fetch("https://example.com/", timeout=5).status)
threads = [threading.Thread(target=run) for _ in range(8)]
for thread in threads:
thread.start()
for thread in threads:
thread.join(10)
self.assertEqual(results, [200] * 8)
def test_network_error_is_reported(self) -> None:
with self.assertRaises(BrowserFetchError) as caught:
self.service.fetch("https://fail.example/", timeout=5)
self.assertNotIsInstance(caught.exception, BrowserFetchUnavailable)
def test_crashed_process_fails_pending_and_restarts(self) -> None:
with self.assertRaises(BrowserFetchError):
self.service.fetch("https://crash.example/", timeout=5)
self.assertEqual(self.service.fetch("https://example.com/", timeout=5).status, 200)
def test_core_without_chromium_library_is_unavailable(self) -> None:
with self.assertRaises(BrowserFetchUnavailable):
self.service.fetch("https://nolib.example/", timeout=5)
def test_missing_core_is_unavailable(self) -> None:
service = BrowserFetchService(executable=lambda: Path("/nonexistent/sing-box.exe"), enabled=True)
self.assertFalse(service.available())
self.assertFalse(BrowserFetchService(executable=lambda: FAKE).available()) # не включена
with self.assertRaises(BrowserFetchUnavailable):
service.fetch("https://example.com/", timeout=1)
if __name__ == "__main__":
unittest.main()