feat(network): migrate async HTTP client to HTTPX2 (#6394)

This commit is contained in:
InfinityPacer
2026-08-22 16:48:23 +08:00
committed by GitHub
parent dfc047b880
commit d131f8d571
9 changed files with 198 additions and 65 deletions
+2 -2
View File
@@ -20,7 +20,7 @@ from urllib.parse import parse_qs, quote, unquote, urlparse, urlsplit
import aiofiles
import aioshutil
import httpx
import httpx2
from anyio import Path as AsyncPath
from packaging.markers import default_environment
from packaging.requirements import Requirement
@@ -2152,7 +2152,7 @@ class PluginHelper(metaclass=WeakSingleton):
async def __async_request_with_fallback(url: str,
headers: Optional[dict] = None,
timeout: Optional[int] = 60,
is_api: bool = False) -> Optional[httpx.Response]:
is_api: bool = False) -> Optional[httpx2.Response]:
"""
使用自动降级策略,异步请求资源,优先级依次为镜像站、代理、直连
:param url: 目标URL
+55 -52
View File
@@ -10,7 +10,7 @@ from pathlib import Path
from typing import Any, Dict, Optional, Tuple, Union
import chardet
import httpx
import httpx2
import requests
import urllib3
from requests import Response, Session
@@ -31,7 +31,14 @@ def configure_default_user_agent(user_agent: str) -> None:
_default_user_agent = user_agent
class _NonClosingTransportProxy(httpx.AsyncBaseTransport):
_ASYNC_STALE_CONNECTION_ERRORS = (
httpx2.RemoteProtocolError,
httpx2.ReadError,
httpx2.WriteError,
)
class _NonClosingTransportProxy(httpx2.AsyncBaseTransport):
"""
包装共享底层 transport,转发请求但吞掉 __aexit__/aclose 调用。
防止 per-call AsyncClient 在 async with 退出时把底层连接池一并清空。
@@ -40,7 +47,7 @@ class _NonClosingTransportProxy(httpx.AsyncBaseTransport):
__slots__ = ("_wrapped",)
def __init__(self, wrapped: httpx.AsyncBaseTransport):
def __init__(self, wrapped: httpx2.AsyncBaseTransport):
"""保存由进程统一管理生命周期的底层传输对象。"""
self._wrapped = wrapped
@@ -58,7 +65,7 @@ class _NonClosingTransportProxy(httpx.AsyncBaseTransport):
# 故意 no-op:调用方显式 aclose 也不影响共享池
return None
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
async def handle_async_request(self, request: httpx2.Request) -> httpx2.Response:
"""将异步请求转发给共享底层传输。"""
return await self._wrapped.handle_async_request(request)
@@ -73,7 +80,7 @@ _SharedTransportKey = Tuple[
]
# 共享底层 transport 桶,按事件循环和配置区分,支持 LRU 淘汰
_shared_async_transports: weakref.WeakKeyDictionary[asyncio.AbstractEventLoop, collections.OrderedDict[_SharedTransportKey, httpx.AsyncHTTPTransport]] = weakref.WeakKeyDictionary()
_shared_async_transports: weakref.WeakKeyDictionary[asyncio.AbstractEventLoop, collections.OrderedDict[_SharedTransportKey, httpx2.AsyncHTTPTransport]] = weakref.WeakKeyDictionary()
# 不同线程各自驱动的事件循环并发首次写入外层弱字典时,需要互斥保护
_shared_async_transports_lock = threading.Lock()
# 每个事件循环允许的最大共享 transport 桶数;超出后按 LRU 淘汰最久未用桶。
@@ -87,7 +94,7 @@ _DEFAULT_KEEPALIVE_EXPIRY = 30
# 同步 requests.Session 复用连接时,遇到对端或代理关闭 keep-alive 后允许重试的方法
_REQUESTS_RETRY_IDEMPOTENT_METHODS = ("GET", "HEAD", "OPTIONS")
# 代理走 CONNECT 隧道时,httpx 默认开启的 HTTP/2 多路复用会把并发请求叠加到极少数隧道上;
# 代理走 CONNECT 隧道时,HTTP/2 多路复用会把并发请求叠加到极少数隧道上;
# 隧道被代理节点切换或空闲回收打断后,复用其上的所有请求会同时失败。按 (proxy, host) 熔断:
# 命中一次连接层失败就记录下次允许再尝试 h2 的时间戳(time.monotonic 基准),冷却期内该
# (proxy, host) 的请求直接退化为 http1.1;冷却期结束后自动恢复尝试 h2。
@@ -98,17 +105,17 @@ _h2_proxy_retry_at: Dict[Tuple[str, str], float] = {}
# EndOfStream 等);超时、连接失败、代理不可达等错误换 h1 一样会发生,
# 不应触发熔断,也不值得付出一次注定同样失败的 h1 重试
_H2_TUNNEL_BREAK_ERRORS = (
httpx.RemoteProtocolError,
httpx.LocalProtocolError,
httpx.ReadError,
httpx.WriteError,
httpx.CloseError,
httpx2.RemoteProtocolError,
httpx2.LocalProtocolError,
httpx2.ReadError,
httpx2.WriteError,
httpx2.CloseError,
)
def _h2_proxy_breaker_key(proxy: str, url: str) -> Tuple[str, str]:
try:
host = httpx.URL(url).host or ""
host = httpx2.URL(url).host or ""
except Exception:
host = url
return proxy, host
@@ -152,7 +159,7 @@ def _get_shared_async_transport(
max_keepalive_connections: int,
max_connections: int,
keepalive_expiry: int,
) -> Optional[httpx.AsyncHTTPTransport]:
) -> Optional[httpx2.AsyncHTTPTransport]:
"""
返回与当前事件循环绑定的共享 AsyncHTTPTransport(底层连接池);首次按需创建。
没有运行中的事件循环或循环已关闭时返回 None,由调用方走临时客户端兜底。
@@ -161,7 +168,7 @@ def _get_shared_async_transport(
会话级状态由调用方在外层 AsyncClient(transport=...) 实例化时单独配置,
每次调用用完即销毁,因此天然无 jar 累积串扰。
"""
# 规范化代理:拒绝空字符串等非法值,防止 httpx 抛出 Unknown scheme for proxy URL
# 规范化代理:拒绝空字符串等非法值,防止客户端解析空代理地址失败
if proxy is not None and (not proxy or not proxy.strip()):
proxy = None
try:
@@ -191,11 +198,11 @@ def _get_shared_async_transport(
return transport
# 首次见到这个配置,创建新的共享 transport 桶
transport = httpx.AsyncHTTPTransport(
transport = httpx2.AsyncHTTPTransport(
http2=http2,
proxy=proxy,
verify=verify,
limits=httpx.Limits(
limits=httpx2.Limits(
max_keepalive_connections=max_keepalive_connections,
max_connections=max_connections,
keepalive_expiry=keepalive_expiry,
@@ -978,7 +985,7 @@ class AsyncRequestUtils:
ua: str = None,
cookies: Union[str, dict] = None,
proxies: dict = None,
client: httpx.AsyncClient = None,
client: httpx2.AsyncClient = None,
timeout: int = None,
referer: str = None,
content_type: str = None,
@@ -995,7 +1002,7 @@ class AsyncRequestUtils:
:param ua: User-Agent字符串
:param cookies: Cookie字符串或字典
:param proxies: 代理设置
:param client: httpx.AsyncClient实例,如果为None则创建新的客户端
:param client: 调用方自管的 HTTPX2 AsyncClient;为空时使用宿主客户端
:param timeout: 请求超时时间,默认为20秒
:param referer: Referer头部信息
:param content_type: 请求的Content-Type,默认为 "application/x-www-form-urlencoded; charset=UTF-8"
@@ -1096,7 +1103,7 @@ class AsyncRequestUtils:
async def request(
self, method: str, url: str, raise_exception: bool = False, **kwargs
) -> Optional[httpx.Response]:
) -> Optional[httpx2.Response]:
"""
发起异步HTTP请求
:param method: HTTP方法,如 get, post, put 等
@@ -1104,7 +1111,7 @@ class AsyncRequestUtils:
:param raise_exception: 是否在发生异常时抛出异常,否则默认拦截异常返回None
:param kwargs: 其他请求参数,如headers, cookies, proxies等
:return: HTTP响应对象
:raises: httpx.RequestError 仅raise_exceptionTrue时抛出
:raises: HTTP 客户端请求异常仅在 raise_exception=True 时抛出
"""
# 运行时 self._cookies 只能是 dict | Nonecookie_parse 默认 array=False 返回 dict
cookies_dict: Optional[dict] = self._cookies if isinstance(self._cookies, dict) else None
@@ -1137,7 +1144,7 @@ class AsyncRequestUtils:
return await self._dispatch_request(
False, cookies_dict, method, url, raise_exception, **kwargs
)
except httpx.RequestError:
except httpx2.RequestError:
# 与 h2 隧道无关的失败(超时、连接失败等):不熔断也不重试,
# 恢复调用方原本的 raise_exception 语义
if raise_exception:
@@ -1147,7 +1154,7 @@ class AsyncRequestUtils:
async def _dispatch_request(
self, http2: bool, cookies_dict: Optional[dict], method: str, url: str,
raise_exception: bool, **kwargs
) -> Optional[httpx.Response]:
) -> Optional[httpx2.Response]:
"""
按给定 http2 开关构建/复用底层连接并发起请求,供 request() 的 h2/h1 熔断切换复用
"""
@@ -1165,9 +1172,9 @@ class AsyncRequestUtils:
if transport is not None:
# 用 _NonClosingTransportProxy 包装共享 transport,吞掉 AsyncClient.__aexit__
# 传播下来的 transport.__aexit__,避免每次 async with 退出都把共享连接池清空。
async with httpx.AsyncClient(
async with httpx2.AsyncClient(
transport=_NonClosingTransportProxy(transport),
timeout=httpx.Timeout(self._timeout),
timeout=httpx2.Timeout(self._timeout),
follow_redirects=self._follow_redirects,
cookies=cookies_dict,
) as client:
@@ -1176,7 +1183,7 @@ class AsyncRequestUtils:
)
# 兜底:没有运行中的事件循环时,临时客户端走完即关
async with httpx.AsyncClient(
async with httpx2.AsyncClient(
http2=http2,
proxy=self._proxies,
timeout=self._timeout,
@@ -1190,12 +1197,12 @@ class AsyncRequestUtils:
async def _make_request(
self,
client: httpx.AsyncClient,
client: httpx2.AsyncClient,
method: str,
url: str,
raise_exception: bool = False,
**kwargs,
) -> Optional[httpx.Response]:
) -> Optional[httpx2.Response]:
"""
执行实际的异步请求
"""
@@ -1213,15 +1220,13 @@ class AsyncRequestUtils:
# 仅对幂等方法做 stale-pool 竞态重试:复用了刚被对端 FIN 的 keep-alive 连接时,
# 实际请求通常未到服务端,httpx 自身不重试,这里兜底一次。
is_idempotent = method_upper in ("GET", "HEAD", "OPTIONS")
stale_conn_errs = (httpx.RemoteProtocolError, httpx.ReadError, httpx.WriteError)
try:
return await client.request(method, url, **kwargs)
except stale_conn_errs:
except _ASYNC_STALE_CONNECTION_ERRORS:
if is_idempotent:
try:
return await client.request(method, url, **kwargs)
except httpx.RequestError:
except httpx2.RequestError:
if raise_exception:
raise
return None
@@ -1230,7 +1235,7 @@ class AsyncRequestUtils:
if raise_exception:
raise
return None
except httpx.RequestError:
except httpx2.RequestError:
if raise_exception:
raise
return None
@@ -1258,7 +1263,7 @@ class AsyncRequestUtils:
async def post(
self, url: str, data: Any = None, json: dict = None, **kwargs
) -> Optional[httpx.Response]:
) -> Optional[httpx2.Response]:
"""
发送异步POST请求
:param url: 请求的URL
@@ -1273,7 +1278,7 @@ class AsyncRequestUtils:
async def put(
self, url: str, data: Any = None, **kwargs
) -> Optional[httpx.Response]:
) -> Optional[httpx2.Response]:
"""
发送异步PUT请求
:param url: 请求的URL
@@ -1292,7 +1297,7 @@ class AsyncRequestUtils:
allow_redirects: bool = True,
raise_exception: bool = False,
**kwargs,
) -> Optional[httpx.Response]:
) -> Optional[httpx2.Response]:
"""
发送异步GET请求并返回响应对象
:param url: 请求的URL
@@ -1303,7 +1308,7 @@ class AsyncRequestUtils:
:param raise_exception: 是否在发生异常时抛出异常,否则默认拦截异常返回None
:param kwargs: 其他请求参数,如headers, cookies, proxies等
:return: HTTP响应对象,若发生RequestError则返回None
:raises: httpx.RequestError 仅raise_exceptionTrue时抛出
:raises: HTTP 客户端请求异常仅在 raise_exception=True 时抛出
"""
return await self.request(
method="get",
@@ -1326,13 +1331,13 @@ class AsyncRequestUtils:
):
"""
获取异步流式响应的上下文管理器,适用于大文件下载。
使用 httpx.AsyncClient.stream() 标准流式 API,避免把响应体一次性读入内存。
使用 AsyncClient.stream() 标准流式 API,避免把响应体一次性读入内存。
:param url: 请求的URL
:param params: 请求的参数
:param raise_exception: 是否在发生异常时抛出,否则吞掉并 yield None
:param kwargs: 其他请求参数(headers, cookies 等)
:return: 上下文管理器,进入后 yield httpx.Response(出错时 yield None
:return: 上下文管理器,进入后返回响应对象(出错时返回 None
"""
cookies_dict: Optional[dict] = self._cookies if isinstance(self._cookies, dict) else None
kwargs["headers"] = with_correlation_header(
@@ -1341,8 +1346,6 @@ class AsyncRequestUtils:
# 与 _make_request 保持一致:复用 keep-alive 时偶遇对端 FIN 的连接,
# 流式 GET 是幂等的,单次重试即可
stale_conn_errs = (httpx.RemoteProtocolError, httpx.ReadError, httpx.WriteError)
async with AsyncExitStack() as stack:
# 选 client:复用与 request() 相同的三条 path 逻辑
if self._client is not None:
@@ -1360,16 +1363,16 @@ class AsyncRequestUtils:
)
if transport is not None:
client = await stack.enter_async_context(
httpx.AsyncClient(
httpx2.AsyncClient(
transport=_NonClosingTransportProxy(transport),
timeout=httpx.Timeout(self._timeout),
timeout=httpx2.Timeout(self._timeout),
follow_redirects=self._follow_redirects,
cookies=cookies_dict,
)
)
else:
client = await stack.enter_async_context(
httpx.AsyncClient(
httpx2.AsyncClient(
http2=self._http2,
proxy=self._proxies,
timeout=self._timeout,
@@ -1383,17 +1386,17 @@ class AsyncRequestUtils:
response = await stack.enter_async_context(
client.stream("GET", url, params=params, **kwargs)
)
except stale_conn_errs:
except _ASYNC_STALE_CONNECTION_ERRORS:
try:
response = await stack.enter_async_context(
client.stream("GET", url, params=params, **kwargs)
)
except httpx.RequestError:
except httpx2.RequestError:
if raise_exception:
raise
yield None
return
except httpx.RequestError:
except httpx2.RequestError:
if raise_exception:
raise
yield None
@@ -1413,7 +1416,7 @@ class AsyncRequestUtils:
json: dict = None,
raise_exception: bool = False,
**kwargs,
) -> Optional[httpx.Response]:
) -> Optional[httpx2.Response]:
"""
发送异步POST请求并返回响应对象
:param url: 请求的URL
@@ -1425,7 +1428,7 @@ class AsyncRequestUtils:
:param raise_exception: 是否在发生异常时抛出异常,否则默认拦截异常返回None
:param kwargs: 其他请求参数,如headers, cookies, proxies等
:return: HTTP响应对象,若发生RequestError则返回None
:raises: httpx.RequestError 仅raise_exceptionTrue时抛出
:raises: HTTP 客户端请求异常仅在 raise_exception=True 时抛出
"""
return await self.request(
method="post",
@@ -1449,7 +1452,7 @@ class AsyncRequestUtils:
json: dict = None,
raise_exception: bool = False,
**kwargs,
) -> Optional[httpx.Response]:
) -> Optional[httpx2.Response]:
"""
发送异步PUT请求并返回响应对象
:param url: 请求的URL
@@ -1461,7 +1464,7 @@ class AsyncRequestUtils:
:param raise_exception: 是否在发生异常时抛出异常,否则默认拦截异常返回None
:param kwargs: 其他请求参数,如headers, cookies, proxies等
:return: HTTP响应对象,若发生RequestError则返回None
:raises: httpx.RequestError 仅raise_exceptionTrue时抛出
:raises: HTTP 客户端请求异常仅在 raise_exception=True 时抛出
"""
return await self.request(
method="put",
@@ -1483,7 +1486,7 @@ class AsyncRequestUtils:
allow_redirects: bool = True,
raise_exception: bool = False,
**kwargs,
) -> Optional[httpx.Response]:
) -> Optional[httpx2.Response]:
"""
发送异步DELETE请求并返回响应对象
:param url: 请求的URL
@@ -1493,7 +1496,7 @@ class AsyncRequestUtils:
:param raise_exception: 是否在发生异常时抛出异常,否则默认拦截异常返回None
:param kwargs: 其他请求参数,如headers, cookies, proxies等
:return: HTTP响应对象,若发生RequestError则返回None
:raises: httpx.RequestError 仅raise_exceptionTrue时抛出
:raises: HTTP 客户端请求异常仅在 raise_exception=True 时抛出
"""
return await self.request(
method="delete",
+4 -2
View File
@@ -8,7 +8,7 @@ from random import choice
from typing import Optional, Union
from urllib import parse
import httpx
import httpx2
import requests
from bs4 import BeautifulSoup
@@ -223,7 +223,9 @@ class DoubanApi(metaclass=WeakSingleton):
return req_url, params
@staticmethod
def _handle_response(resp: Union[requests.Response, httpx.Response]) -> dict:
def _handle_response(
resp: Union[requests.Response, httpx2.Response]
) -> dict:
"""
处理HTTP响应
"""
+11
View File
@@ -122,6 +122,17 @@ dependencies = ["example-package>=1,<2"]
- 仅有 `requirements.txt` 的历史插件继续按原方式安装;
- 宿主不消费插件自己的 `uv.lock`,因为多个插件共享同一主程序环境,不能分别同步独立锁文件。
### 3.2 异步 HTTP 客户端边界
主程序自建的 `AsyncRequestUtils` 使用 HTTPX2`app.sdk.network.AsyncRequestUtils` 与旧插件
入口 `app.utils.http.AsyncRequestUtils` 共享同一实现。未显式传入客户端时,返回的响应与抛出的
请求异常均来自 `httpx2`;直接依赖响应类型或异常类型的 V3 代码应导入 `httpx2`
OpenAI、Anthropic、Google GenAI、LangChain、CloakBrowser 等第三方 SDK 继续使用它们声明的
HTTPX 版本。不得调用 `httpx2.alias_httpx()` 在进程内替换 `httpx`,否则会同时改变第三方 SDK、
测试工具和插件的导入结果。确需复用调用方自管客户端时,向 `AsyncRequestUtils` 传入
`httpx2.AsyncClient`
### 4. 准备资源与插件目录
本地源码开发时,主程序需要读取资源文件和插件源码。相关文件需要放到主程序实际加载的目录下:
+1
View File
@@ -33,6 +33,7 @@ dependencies = [
"fastapi~=0.141.1",
"google-genai~=2.8.0",
"httpx[http2,socks]~=0.28.1",
"httpx2[http2,socks]~=2.12.0",
"jieba-next~=1.0.0rc1",
"jinja2~=3.1.6",
"langchain~=1.3.15",
+73 -5
View File
@@ -1,11 +1,12 @@
import asyncio
import time
import httpx
import httpx2
import pytest
from app.adapters.network import http as http_module
from app.adapters.network.http import AsyncRequestUtils
from app.sdk.network import AsyncRequestUtils as SdkAsyncRequestUtils
PROXY = "http://proxy.example:7890"
URL = "https://raw.githubusercontent.com/demo/repo/main/package.json"
@@ -24,7 +25,7 @@ def _fake_dispatch(calls, fail_when):
calls.append(http2)
if fail_when(http2):
if raise_exception:
raise httpx.RemoteProtocolError("tunnel closed")
raise httpx2.RemoteProtocolError("tunnel closed")
return None
return "ok"
@@ -92,7 +93,7 @@ def test_timeout_does_not_trip_breaker_or_retry(monkeypatch):
async def fake(_self, http2, _cookies_dict, _method, _url, _raise_exception, **_kwargs):
calls.append(http2)
raise httpx.ConnectTimeout("proxy slow")
raise httpx2.ConnectTimeout("proxy slow")
monkeypatch.setattr(AsyncRequestUtils, "_dispatch_request", fake)
@@ -112,12 +113,12 @@ def test_timeout_still_raises_when_raise_exception_enabled(monkeypatch):
async def fake(_self, http2, _cookies_dict, _method, _url, _raise_exception, **_kwargs):
calls.append(http2)
raise httpx.ConnectTimeout("proxy slow")
raise httpx2.ConnectTimeout("proxy slow")
monkeypatch.setattr(AsyncRequestUtils, "_dispatch_request", fake)
utils = AsyncRequestUtils(proxies={"https": PROXY})
with pytest.raises(httpx.ConnectTimeout):
with pytest.raises(httpx2.ConnectTimeout):
asyncio.run(utils.request("get", URL, raise_exception=True))
assert calls == [True]
@@ -154,3 +155,70 @@ def test_no_proxy_configured_skips_breaker_logic(monkeypatch):
assert result == "ok"
assert calls == [True]
def test_internal_client_uses_httpx2_transport(monkeypatch):
"""宿主自建客户端使用 HTTPX2,并返回对应响应对象。"""
async def respond(request: httpx2.Request) -> httpx2.Response:
return httpx2.Response(200, json={"client": "httpx2"}, request=request)
monkeypatch.setattr(
http_module,
"_get_shared_async_transport",
lambda **_kwargs: httpx2.MockTransport(respond),
)
response = asyncio.run(
AsyncRequestUtils().get_res("https://example.com/data", raise_exception=True)
)
assert isinstance(response, httpx2.Response)
assert response.json() == {"client": "httpx2"}
def test_plugin_sdk_uses_httpx2_by_default(monkeypatch):
"""插件 SDK 默认复用宿主 HTTPX2 客户端合同。"""
async def respond(request: httpx2.Request) -> httpx2.Response:
return httpx2.Response(200, request=request)
monkeypatch.setattr(
http_module,
"_get_shared_async_transport",
lambda **_kwargs: httpx2.MockTransport(respond),
)
response = asyncio.run(
SdkAsyncRequestUtils().get_res(
"https://example.com/sdk", raise_exception=True
)
)
assert SdkAsyncRequestUtils is AsyncRequestUtils
assert isinstance(response, httpx2.Response)
def test_legacy_http_module_uses_httpx2_by_default(monkeypatch):
"""旧插件 HTTP 导入入口随宿主默认客户端迁移到 HTTPX2。"""
import importlib
legacy_http = importlib.import_module("app.utils.http")
async def respond(request: httpx2.Request) -> httpx2.Response:
return httpx2.Response(200, request=request)
monkeypatch.setattr(
http_module,
"_get_shared_async_transport",
lambda **_kwargs: httpx2.MockTransport(respond),
)
response = asyncio.run(
legacy_http.AsyncRequestUtils().get_res(
"https://example.com/legacy", raise_exception=True
)
)
assert legacy_http.AsyncRequestUtils is AsyncRequestUtils
assert isinstance(response, httpx2.Response)
+4 -3
View File
@@ -6,6 +6,7 @@ from types import SimpleNamespace
from unittest.mock import MagicMock
import httpx
import httpx2
import pytest
from starlette.applications import Starlette
from starlette.responses import JSONResponse, StreamingResponse
@@ -162,12 +163,12 @@ async def test_async_external_request_preserves_explicit_header() -> None:
"""异步外呼默认传播当前 ID,但不得覆盖调用方显式 trace 边界。"""
observed = []
async def respond(request: httpx.Request) -> httpx.Response:
async def respond(request: httpx2.Request) -> httpx2.Response:
"""记录 MockTransport 收到的请求头。"""
observed.append(request.headers[CORRELATION_ID_HEADER])
return httpx.Response(200)
return httpx2.Response(200)
async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as client:
async with httpx2.AsyncClient(transport=httpx2.MockTransport(respond)) as client:
utils = AsyncRequestUtils(client=client)
with correlation_scope("context-request"):
await utils.request("GET", "https://example.com/default")
+1 -1
View File
@@ -658,7 +658,7 @@ def test_shared_http_close_waits_for_real_lru_eviction(monkeypatch):
raise RuntimeError("eviction close failed")
monkeypatch.setattr(http_utils, "_MAX_SHARED_TRANSPORTS_PER_LOOP", 1)
monkeypatch.setattr(http_utils.httpx, "AsyncHTTPTransport", FakeTransport)
monkeypatch.setattr(http_utils.httpx2, "AsyncHTTPTransport", FakeTransport)
async def run_test():
transport_kwargs = {
"proxy": None,
Generated
+47
View File
@@ -967,6 +967,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpcore2"
version = "2.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11" },
{ name = "truststore" },
]
sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
@@ -1003,6 +1016,29 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
]
[[package]]
name = "httpx2"
version = "2.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpcore2" },
{ name = "idna" },
{ name = "truststore" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" },
]
[package.optional-dependencies]
http2 = [
{ name = "h2" },
]
socks = [
{ name = "socksio" },
]
[[package]]
name = "hyperframe"
version = "6.1.0"
@@ -1500,6 +1536,7 @@ dependencies = [
{ name = "fastapi" },
{ name = "google-genai" },
{ name = "httpx", extra = ["http2", "socks"] },
{ name = "httpx2", extra = ["http2", "socks"] },
{ name = "jieba-next" },
{ name = "jinja2" },
{ name = "langchain" },
@@ -1609,6 +1646,7 @@ requires-dist = [
{ name = "fastapi", specifier = "~=0.141.1" },
{ name = "google-genai", specifier = "~=2.8.0" },
{ name = "httpx", extras = ["http2", "socks"], specifier = "~=0.28.1" },
{ name = "httpx2", extras = ["http2", "socks"], specifier = "~=2.12.0" },
{ name = "jieba-next", specifier = "~=1.0.0rc1" },
{ name = "jinja2", specifier = "~=3.1.6" },
{ name = "langchain", specifier = "~=1.3.15" },
@@ -2977,6 +3015,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/80/9c/edc8ee0d9ab7a05f0cb334fc506ce3749ff869a8d41683108313096ce5ab/transmission_rpc-7.0.12-py3-none-any.whl", hash = "sha256:85e26b7ca13e1e102a695fe19f19f4236bd42bb4920b0edb9e35f6bb46bbe86e", size = 29103, upload-time = "2026-08-09T14:37:40.272Z" },
]
[[package]]
name = "truststore"
version = "0.10.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
]
[[package]]
name = "typing-extensions"
version = "4.16.0"