fix(custom_httpx): preserve non-ascii response headers

This commit is contained in:
Yancey Zhou 2026-04-26 21:01:30 +08:00
parent 084acdadad
commit 45336b0179
5 changed files with 74 additions and 6 deletions

View file

@ -341,7 +341,7 @@ class LiteLLMAiohttpTransport(AiohttpTransport):
return httpx.Response(
status_code=response.status,
headers=response.headers,
headers=response.raw_headers,
stream=AiohttpResponseStream(response),
request=request,
)

View file

@ -381,11 +381,11 @@ class MaskedHTTPStatusError(httpx.HTTPStatusError):
except Exception:
response_content = b""
response_headers = {
k: v
for k, v in original_error.response.headers.items()
if k.lower() not in ("content-encoding", "content-length")
}
response_headers = [
(k, v)
for k, v in original_error.response.headers.raw
if k.lower() not in (b"content-encoding", b"content-length")
]
masked_request = httpx.Request(
method=original_error.request.method,

View file

@ -238,6 +238,7 @@ async def test_handle_async_request_uses_env_proxy(monkeypatch):
class Resp:
status = 200
headers = {}
raw_headers = ()
async def __aenter__(self):
return self
@ -262,6 +263,51 @@ async def test_handle_async_request_uses_env_proxy(monkeypatch):
assert captured["proxy"] == proxy_url
@pytest.mark.asyncio
async def test_handle_async_request_preserves_non_ascii_response_headers():
"""Aiohttp transport should pass raw response header bytes through to httpx."""
header_value = "本地化消息"
class FakeSession:
closed = False
def __init__(self):
try:
self._loop = asyncio.get_running_loop()
except RuntimeError:
self._loop = None
def request(self, *args, **kwargs):
class Resp:
status = 200
headers = {"X-Localized-Message": header_value}
raw_headers = ((b"X-Localized-Message", header_value.encode("utf-8")),)
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
pass
@property
def content(self):
class C:
async def iter_chunked(self, size):
yield b"ok"
return C()
return Resp()
transport = LiteLLMAiohttpTransport(client=lambda: FakeSession()) # type: ignore
response = await transport.handle_async_request(
httpx.Request("GET", "http://example.com/asset")
)
assert response.status_code == 200
assert response.headers.get("x-localized-message") == header_value
@pytest.mark.asyncio
async def test_handle_async_request_uses_env_proxy_per_url(monkeypatch):
"""Aiohttp transport should honor HTTP(S)_PROXY env vars unless NO_PROXY matches"""
@ -295,6 +341,7 @@ async def test_handle_async_request_uses_env_proxy_per_url(monkeypatch):
class Resp:
status = 200
headers = {}
raw_headers = ()
async def __aenter__(self):
return self
@ -353,6 +400,7 @@ def _make_mock_response(should_fail=False, fail_count={"count": 0}):
class MockResp:
status = 200
headers = {}
raw_headers = ()
async def __aenter__(self):
if should_fail and fail_count["count"] < 1:

View file

@ -135,6 +135,24 @@ class TestMaskedHTTPStatusError:
# Content-Encoding must have been stripped from the rebuilt headers.
assert "content-encoding" not in {k.lower() for k in masked.response.headers}
def test_preserves_non_ascii_response_headers(self):
header_value = "本地化错误消息"
request = httpx.Request("GET", "https://api.example.com?key=SECRET")
response = httpx.Response(
status_code=400,
content=b"bad request",
headers=[
(b"x-localized-message", header_value.encode("utf-8")),
],
request=request,
)
orig = httpx.HTTPStatusError("400", request=request, response=response)
masked = MaskedHTTPStatusError(orig)
assert masked.response.headers.get("x-localized-message") == header_value
assert "SECRET" not in str(masked.response.request.url)
class TestSafeResponseHelpers:
def test_safe_get_response_text_normal(self):

View file

@ -40,6 +40,7 @@ async def test_aiohttp_transport_response_uses_stream_not_content():
class Resp:
status = 200
headers = {}
raw_headers = ()
async def __aenter__(self):
return self
@ -73,6 +74,7 @@ async def test_aiohttp_response_stream_aclose_releases_connection():
class MockResponse:
status = 200
headers = {}
raw_headers = ()
@property
def content(self):