From f3eb69308d5daee7e4f7d10493996cb7717551ef Mon Sep 17 00:00:00 2001 From: genz27 Date: Sun, 29 Mar 2026 18:49:16 +0800 Subject: [PATCH] Fix remote browser JSON control-plane delivery --- src/api/admin.py | 6 ++++-- src/services/flow_client.py | 6 ++++-- tests/test_api_routes.py | 15 +++++++++++---- tests/test_flow_client_control_plane.py | 15 +++++++++++---- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/api/admin.py b/src/api/admin.py index c4ad4ba..5561305 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -1,6 +1,7 @@ """Admin API routes""" import asyncio import json +import httpx from fastapi import APIRouter, Depends, HTTPException, Header, Request from fastapi.responses import JSONResponse from pydantic import BaseModel @@ -189,7 +190,6 @@ async def _sync_json_http_request( request_kwargs: Dict[str, Any] = { "headers": req_headers, "timeout": timeout, - "impersonate": "chrome120", } if payload is not None: @@ -198,7 +198,9 @@ async def _sync_json_http_request( request_kwargs["json"] = payload try: - async with AsyncSession() as session: + # remote_browser 控制面是服务间 JSON API,使用 httpx 避免 curl_cffi 在当前 + # Windows + impersonate 场景下 POST body 丢失导致 FastAPI 直接判定 body 缺失。 + async with httpx.AsyncClient(follow_redirects=True) as session: response = await session.request( method=request_method, url=url, diff --git a/src/services/flow_client.py b/src/services/flow_client.py index 2d31a7b..f75a6f3 100644 --- a/src/services/flow_client.py +++ b/src/services/flow_client.py @@ -2,6 +2,7 @@ import asyncio import json import contextvars +import httpx import time import uuid import random @@ -2040,7 +2041,6 @@ class FlowClient: request_kwargs: Dict[str, Any] = { "headers": req_headers, "timeout": timeout, - "impersonate": "chrome120", } if payload is not None: @@ -2049,7 +2049,9 @@ class FlowClient: request_kwargs["json"] = payload try: - async with AsyncSession() as session: + # remote_browser 控制面只需要稳定传输 JSON,不需要浏览器指纹伪装。 + # 使用 httpx 可以避免 curl_cffi 在当前环境下 POST body 被吞掉。 + async with httpx.AsyncClient(follow_redirects=True) as session: response = await session.request( method=request_method, url=url, diff --git a/tests/test_api_routes.py b/tests/test_api_routes.py index cb6a159..e889c13 100644 --- a/tests/test_api_routes.py +++ b/tests/test_api_routes.py @@ -85,7 +85,7 @@ def test_flexible_auth_accepts_x_goog_api_key(monkeypatch): ) == "secret" -def test_admin_remote_browser_helper_uses_asyncsession(monkeypatch): +def test_admin_remote_browser_helper_uses_httpx(monkeypatch): calls = [] class FakeResponse: @@ -95,7 +95,10 @@ def test_admin_remote_browser_helper_uses_asyncsession(monkeypatch): def json(self): return {"success": True, "token": "abc"} - class FakeSession: + class FakeAsyncClient: + def __init__(self, **kwargs): + calls.append({"client_kwargs": kwargs}) + async def __aenter__(self): return self @@ -110,7 +113,7 @@ def test_admin_remote_browser_helper_uses_asyncsession(monkeypatch): }) return FakeResponse() - monkeypatch.setattr(admin_module, "AsyncSession", FakeSession) + monkeypatch.setattr(admin_module.httpx, "AsyncClient", FakeAsyncClient) status_code, payload, response_text = asyncio.run( admin_module._sync_json_http_request( @@ -126,6 +129,11 @@ def test_admin_remote_browser_helper_uses_asyncsession(monkeypatch): assert payload == {"success": True, "token": "abc"} assert response_text == '{"success": true, "token": "abc"}' assert calls == [ + { + "client_kwargs": { + "follow_redirects": True, + }, + }, { "method": "POST", "url": "https://example.com/api/v1/custom-score", @@ -136,7 +144,6 @@ def test_admin_remote_browser_helper_uses_asyncsession(monkeypatch): "Content-Type": "application/json; charset=utf-8", }, "timeout": 15, - "impersonate": "chrome120", "json": {"website_url": "https://example.com"}, }, } diff --git a/tests/test_flow_client_control_plane.py b/tests/test_flow_client_control_plane.py index 4d1ec8d..ae0ba4e 100644 --- a/tests/test_flow_client_control_plane.py +++ b/tests/test_flow_client_control_plane.py @@ -49,7 +49,7 @@ def test_control_plane_calls_use_short_timeouts(monkeypatch): assert [call["timeout"] for call in calls] == [10, 15, 10, 10] -def test_remote_browser_http_helper_uses_asyncsession(monkeypatch): +def test_remote_browser_http_helper_uses_httpx(monkeypatch): calls = [] class FakeResponse: @@ -59,7 +59,10 @@ def test_remote_browser_http_helper_uses_asyncsession(monkeypatch): def json(self): return {"ok": True} - class FakeSession: + class FakeAsyncClient: + def __init__(self, **kwargs): + calls.append({"client_kwargs": kwargs}) + async def __aenter__(self): return self @@ -74,7 +77,7 @@ def test_remote_browser_http_helper_uses_asyncsession(monkeypatch): }) return FakeResponse() - monkeypatch.setattr(flow_client_module, "AsyncSession", FakeSession) + monkeypatch.setattr(flow_client_module.httpx, "AsyncClient", FakeAsyncClient) status_code, payload, response_text = asyncio.run( FlowClient._sync_json_http_request( @@ -90,6 +93,11 @@ def test_remote_browser_http_helper_uses_asyncsession(monkeypatch): assert payload == {"ok": True} assert response_text == '{"ok": true}' assert calls == [ + { + "client_kwargs": { + "follow_redirects": True, + }, + }, { "method": "POST", "url": "https://example.com/api/v1/solve", @@ -100,7 +108,6 @@ def test_remote_browser_http_helper_uses_asyncsession(monkeypatch): "Content-Type": "application/json; charset=utf-8", }, "timeout": 12, - "impersonate": "chrome120", "json": {"project_id": "project-123"}, }, }