mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(custom_httpx): preserve non-ascii response headers
This commit is contained in:
parent
084acdadad
commit
45336b0179
5 changed files with 74 additions and 6 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue