Skip to content

Commit b7859dd

Browse files
committed
fix: fallback when httpx is unavailable
1 parent 5d8c0d4 commit b7859dd

3 files changed

Lines changed: 157 additions & 17 deletions

File tree

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ uvicorn[standard]==0.32.1
33
aiosqlite==0.20.0
44
pydantic==2.10.4
55
curl-cffi==0.7.3
6+
httpx>=0.27.0,<1.0.0
67
tomli==2.2.1
78
bcrypt==4.2.1
89
python-multipart==0.0.20

src/api/admin.py

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""Admin API routes"""
22
import asyncio
33
import json
4-
import httpx
54
from fastapi import APIRouter, Depends, HTTPException, Header, Request
65
from fastapi.responses import JSONResponse
76
from pydantic import BaseModel
87
from typing import Optional, List, Dict, Any
98
import secrets
109
import time
1110
import re
11+
import urllib.error
12+
import urllib.request
1213
from urllib.parse import urlparse
1314
from curl_cffi.requests import AsyncSession
1415
from ..core.auth import AuthManager
@@ -18,6 +19,11 @@
1819
from ..services.proxy_manager import ProxyManager
1920
from ..services.concurrency_manager import ConcurrencyManager
2021

22+
try:
23+
import httpx
24+
except ImportError:
25+
httpx = None
26+
2127
router = APIRouter()
2228

2329
# Dependency injection
@@ -177,6 +183,72 @@ def _get_remote_browser_client_config() -> tuple[str, str, int]:
177183
return base_url, api_key, timeout
178184

179185

186+
def _build_remote_browser_http_timeout(read_timeout: float) -> Any:
187+
read_value = max(3.0, float(read_timeout))
188+
write_value = min(10.0, max(3.0, read_value))
189+
if httpx is None:
190+
return read_value
191+
return httpx.Timeout(
192+
connect=2.5,
193+
read=read_value,
194+
write=write_value,
195+
pool=2.5,
196+
)
197+
198+
199+
def _parse_json_response_text(text: str) -> Optional[Any]:
200+
if not text:
201+
return None
202+
try:
203+
return json.loads(text)
204+
except Exception:
205+
return None
206+
207+
208+
async def _stdlib_json_http_request(
209+
method: str,
210+
url: str,
211+
headers: Dict[str, str],
212+
payload: Optional[Dict[str, Any]],
213+
timeout: int,
214+
) -> tuple[int, Optional[Any], str]:
215+
req_headers = dict(headers or {})
216+
req_headers.setdefault("Accept", "application/json")
217+
request_method = (method or "GET").upper()
218+
request_data: Optional[bytes] = None
219+
220+
if payload is not None:
221+
req_headers["Content-Type"] = "application/json; charset=utf-8"
222+
if request_method != "GET":
223+
request_data = json.dumps(payload).encode("utf-8")
224+
225+
def do_request() -> tuple[int, str]:
226+
request = urllib.request.Request(
227+
url=url,
228+
data=request_data,
229+
headers=req_headers,
230+
method=request_method,
231+
)
232+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
233+
try:
234+
with opener.open(request, timeout=max(1.0, float(timeout))) as response:
235+
status_code = int(getattr(response, "status", 0) or response.getcode() or 0)
236+
body = response.read()
237+
charset = response.headers.get_content_charset() or "utf-8"
238+
return status_code, body.decode(charset, errors="replace")
239+
except urllib.error.HTTPError as exc:
240+
body = exc.read()
241+
charset = exc.headers.get_content_charset() if exc.headers else None
242+
return int(getattr(exc, "code", 0) or 0), body.decode(charset or "utf-8", errors="replace")
243+
244+
try:
245+
status_code, text = await asyncio.to_thread(do_request)
246+
except Exception as e:
247+
raise RuntimeError(f"远程打码服务请求失败: {e}") from e
248+
249+
return status_code, _parse_json_response_text(text), text
250+
251+
180252
async def _sync_json_http_request(
181253
method: str,
182254
url: str,
@@ -189,18 +261,27 @@ async def _sync_json_http_request(
189261
request_method = (method or "GET").upper()
190262
request_kwargs: Dict[str, Any] = {
191263
"headers": req_headers,
192-
"timeout": timeout,
264+
"timeout": _build_remote_browser_http_timeout(timeout),
193265
}
194266

195267
if payload is not None:
196268
req_headers["Content-Type"] = "application/json; charset=utf-8"
197269
if request_method != "GET":
198270
request_kwargs["json"] = payload
199271

272+
if httpx is None:
273+
return await _stdlib_json_http_request(
274+
method=method,
275+
url=url,
276+
headers=req_headers,
277+
payload=payload,
278+
timeout=timeout,
279+
)
280+
200281
try:
201282
# remote_browser 控制面是服务间 JSON API,使用 httpx 避免 curl_cffi 在当前
202283
# Windows + impersonate 场景下 POST body 丢失导致 FastAPI 直接判定 body 缺失。
203-
async with httpx.AsyncClient(follow_redirects=True) as session:
284+
async with httpx.AsyncClient(follow_redirects=False, trust_env=False) as session:
204285
response = await session.request(
205286
method=request_method,
206287
url=url,
@@ -211,12 +292,7 @@ async def _sync_json_http_request(
211292

212293
status_code = int(getattr(response, "status_code", 0) or 0)
213294
text = response.text or ""
214-
parsed: Optional[Any] = None
215-
if text:
216-
try:
217-
parsed = response.json()
218-
except Exception:
219-
parsed = None
295+
parsed = _parse_json_response_text(text)
220296

221297
return status_code, parsed, text
222298

src/services/flow_client.py

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import asyncio
33
import json
44
import contextvars
5-
import httpx
65
import time
76
import uuid
87
import random
@@ -16,6 +15,11 @@
1615
from ..core.logger import debug_logger
1716
from ..core.config import config
1817

18+
try:
19+
import httpx
20+
except ImportError:
21+
httpx = None
22+
1923

2024
class FlowClient:
2125
"""VideoFX API客户端"""
@@ -2031,16 +2035,71 @@ def _get_remote_browser_service_config(self) -> tuple[str, str, int]:
20312035
return base_url, api_key, timeout
20322036

20332037
@staticmethod
2034-
def _build_remote_browser_http_timeout(read_timeout: float) -> httpx.Timeout:
2038+
def _build_remote_browser_http_timeout(read_timeout: float) -> Any:
20352039
read_value = max(3.0, float(read_timeout))
20362040
write_value = min(10.0, max(3.0, read_value))
2041+
if httpx is None:
2042+
return read_value
20372043
return httpx.Timeout(
20382044
connect=2.5,
20392045
read=read_value,
20402046
write=write_value,
20412047
pool=2.5,
20422048
)
20432049

2050+
@staticmethod
2051+
def _parse_json_response_text(text: str) -> Optional[Any]:
2052+
if not text:
2053+
return None
2054+
try:
2055+
return json.loads(text)
2056+
except Exception:
2057+
return None
2058+
2059+
@staticmethod
2060+
async def _stdlib_json_http_request(
2061+
method: str,
2062+
url: str,
2063+
headers: Dict[str, str],
2064+
payload: Optional[Dict[str, Any]],
2065+
timeout: int,
2066+
) -> tuple[int, Optional[Any], str]:
2067+
req_headers = dict(headers or {})
2068+
req_headers.setdefault("Accept", "application/json")
2069+
request_method = (method or "GET").upper()
2070+
request_data: Optional[bytes] = None
2071+
2072+
if payload is not None:
2073+
req_headers["Content-Type"] = "application/json; charset=utf-8"
2074+
if request_method != "GET":
2075+
request_data = json.dumps(payload).encode("utf-8")
2076+
2077+
def do_request() -> tuple[int, str]:
2078+
request = urllib.request.Request(
2079+
url=url,
2080+
data=request_data,
2081+
headers=req_headers,
2082+
method=request_method,
2083+
)
2084+
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
2085+
try:
2086+
with opener.open(request, timeout=max(1.0, float(timeout))) as response:
2087+
status_code = int(getattr(response, "status", 0) or response.getcode() or 0)
2088+
body = response.read()
2089+
charset = response.headers.get_content_charset() or "utf-8"
2090+
return status_code, body.decode(charset, errors="replace")
2091+
except urllib.error.HTTPError as exc:
2092+
body = exc.read()
2093+
charset = exc.headers.get_content_charset() if exc.headers else None
2094+
return int(getattr(exc, "code", 0) or 0), body.decode(charset or "utf-8", errors="replace")
2095+
2096+
try:
2097+
status_code, text = await asyncio.to_thread(do_request)
2098+
except Exception as e:
2099+
raise RuntimeError(f"remote_browser 请求失败: {e}") from e
2100+
2101+
return status_code, FlowClient._parse_json_response_text(text), text
2102+
20442103
@staticmethod
20452104
async def _sync_json_http_request(
20462105
method: str,
@@ -2062,6 +2121,15 @@ async def _sync_json_http_request(
20622121
if request_method != "GET":
20632122
request_kwargs["json"] = payload
20642123

2124+
if httpx is None:
2125+
return await FlowClient._stdlib_json_http_request(
2126+
method=method,
2127+
url=url,
2128+
headers=req_headers,
2129+
payload=payload,
2130+
timeout=timeout,
2131+
)
2132+
20652133
try:
20662134
# remote_browser 控制面只需要稳定传输 JSON,不需要浏览器指纹伪装。
20672135
# 使用 httpx 可以避免 curl_cffi 在当前环境下 POST body 被吞掉。
@@ -2076,12 +2144,7 @@ async def _sync_json_http_request(
20762144

20772145
status_code = int(getattr(response, "status_code", 0) or 0)
20782146
text = response.text or ""
2079-
parsed: Optional[Any] = None
2080-
if text:
2081-
try:
2082-
parsed = response.json()
2083-
except Exception:
2084-
parsed = None
2147+
parsed = FlowClient._parse_json_response_text(text)
20852148

20862149
return status_code, parsed, text
20872150

0 commit comments

Comments
 (0)