-
Notifications
You must be signed in to change notification settings - Fork 2
Open
Description
I am trying to use httpx-aiohttp, and am hitting an incompatibility with vcrpy + httpx_aiohttp.AiohttpResponseStream. What happens is:
- The inner
aiohttprequest has its response modified via thisvcrpypatch to bereadhere.vcrpydoes this so the response can be stored in a VCR cassette.
- That
read(1) exhausts theaiohttp.StreamReaderand (2) moves the data toaiohttp.ClientResponse._body(link). - The
httpx.Responsethen returned fromhttpx_aiohttp.AiohttpTransport.handle_async_requestis broken, because calls toareadreturnb"". Any downstream users ofresponse.json()will get blown up due tojson.loads("")throwing aJSONDecodeError.- This
b""return happens because theAiohttpResponseStream.__aiter__doesn't look at_body(link).
- This
Here is a minimal reproducer:
import httpx_aiohttp
import pytest
@pytest.mark.vcr
@pytest.mark.asyncio
async def test_compressed_get() -> None:
"""Make a GET request, with gzip compression."""
async with httpx_aiohttp.HttpxAiohttpClient(timeout=60) as client:
# Can also use https://httpbin.org/gzip, but it can be flaky
response = await client.get("https://httpbingo.org/gzip")
response.raise_for_status()
assert response.status_code == 200
assert response.json()["gzipped"]Running this code with Python 3.13, pytest==8.4.1, pytest-asyncio==1.1.0, pytest-recording==0.13.4, vcrpy==7.0.0, httpx==0.28.1, and httpx-aiohttp==0.1.8, httpcore==1.0.9, and no-prerecorded VCR cassette, the .json() call at the end leads to:
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
If you realize, this is actually the same issue we recently solved with hishel: karpetrosyan/hishel#374
A possible starter solution for AiohttpResponseStream is:
class AiohttpResponseStream(httpx.AsyncByteStream):
CHUNK_SIZE = 1024 * 16
def __init__(self, aiohttp_response: ClientResponse) -> None:
self._aiohttp_response = aiohttp_response
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
with map_aiohttp_exceptions():
if self._aiohttp_response._body is not None:
# Happens if some intermediary called `await _aiohttp_response.read()`
# TODO: take into account chunk size
# TODO: take into account `not self._aiohttp_response.content.at_eof()`?
yield self._aiohttp_response._body
else:
async for chunk in self._aiohttp_response.content.iter_chunked(self.CHUNK_SIZE):
yield chunkAnd lastly, I would like to say thank you for making this super useful library! Happy it exists and looking forward to using it.
Metadata
Metadata
Assignees
Labels
No labels