From d6d2473668bf7ec0fa7daf3e0960554d7e37dc9d Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Fri, 25 Sep 2026 20:33:35 -0700 Subject: [PATCH] fix(http): serve a body the transport already read through the bounded reader --- litellm/llms/custom_httpx/http_handler.py | 13 +++++++ .../llms/custom_httpx/test_http_handler.py | 39 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/litellm/llms/custom_httpx/http_handler.py b/litellm/llms/custom_httpx/http_handler.py index e6c5749efe6..b75dcc050d1 100644 --- a/litellm/llms/custom_httpx/http_handler.py +++ b/litellm/llms/custom_httpx/http_handler.py @@ -630,6 +630,17 @@ async def _wire_bounded(response: httpx.Response, limit: int) -> AsyncGenerator[ yield chunk +def _already_read_within(response: httpx.Response, max_bytes: int) -> httpx.Response: + if len(response.content) > max_bytes: + raise HTTPResponseLimitError("Response exceeds the configured size limit") + return httpx.Response( + response.status_code, + headers=_headers_of_the_decoded_body(response.headers), + content=response.content, + request=response.request, + ) + + async def _decoded_within(response: httpx.Response, wire: AsyncGenerator[bytes, None], max_bytes: int) -> bytes: decoding: Final = httpx.Response( response.status_code, headers=response.headers, content=wire, request=response.request @@ -817,6 +828,8 @@ class AsyncHTTPHandler: ) if response.is_redirect or response.is_error: return httpx.Response(response.status_code, headers=response.headers, request=response.request) + if response.is_stream_consumed: + return _already_read_within(response, max_bytes) wire_limit: Final = _wire_byte_limit(response.headers, max_bytes) if int(response.headers.get("content-length", "0")) > wire_limit: raise HTTPResponseLimitError("Response exceeds the configured size limit") diff --git a/tests/unit/llms/custom_httpx/test_http_handler.py b/tests/unit/llms/custom_httpx/test_http_handler.py index c095ae3762f..35e5728db42 100644 --- a/tests/unit/llms/custom_httpx/test_http_handler.py +++ b/tests/unit/llms/custom_httpx/test_http_handler.py @@ -1839,6 +1839,45 @@ async def test_bounded_get_caps_the_wire_bytes_of_a_compressed_body_that_decodes await handler.close() +@pytest.mark.asyncio +async def test_bounded_get_serves_a_body_the_transport_already_read(monkeypatch): + monkeypatch.setenv("DISABLE_AIOHTTP_TRANSPORT", "True") + card: Final = {"data": [{"id": "served", "max_model_len": 8192}]} + + def respond(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, json=card) + + handler = AsyncHTTPHandler() + handler.client = httpx.AsyncClient(transport=httpx.MockTransport(respond)) + try: + response = await handler.get("https://one.test/v1/models", max_response_bytes=2 * 1024 * 1024) + finally: + await handler.close() + assert response.json() == card + assert response.headers["content-length"] == str(len(response.content)) + + +@pytest.mark.asyncio +async def test_bounded_get_bounds_an_already_read_body_on_its_decoded_length(monkeypatch): + monkeypatch.setenv("DISABLE_AIOHTTP_TRANSPORT", "True") + document: Final = b"0" * 4096 + + def respond(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=gzip.compress(document), headers={"content-encoding": "gzip"}) + + handler = AsyncHTTPHandler() + handler.client = httpx.AsyncClient(transport=httpx.MockTransport(respond)) + try: + served = await handler.get("https://cdn.example/notes.txt", max_response_bytes=len(document)) + assert served.content == document + assert "content-encoding" not in served.headers + assert served.headers["content-length"] == str(len(document)) + with pytest.raises(HTTPResponseLimitError, match="size limit"): + await handler.get("https://cdn.example/notes.txt", max_response_bytes=len(document) - 1) + finally: + await handler.close() + + @pytest.mark.asyncio async def test_bounded_get_rejects_a_declared_compressed_length_over_the_wire_limit(respx_mock, monkeypatch): monkeypatch.setenv("DISABLE_AIOHTTP_TRANSPORT", "True")