You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
An exception in StreamContextManager.__aclose__() overrides the original #1464
The bug is reproducible against the latest release and/or master.
There are no similar issues or pull requests to fix it yet.
Describe the bug
I alluded to this in #1463. That issue is about the type of exception being raised; this bug is about the source of the exception being raised.
In this code block:
async with httpx_client.stream("GET", url) as response:
async for blob in response.aiter_raw():
pass
# ... Now, imagine `aiter_raw()` raises an exception...
# ... and imagine `response.aclose()` raises an exception within `__aclose__()`.
The exception from aiter_raw() is caught by __aclose__(); the exception from response.aclose() is the one I see.
To reproduce
See #1463 for example code that causes a "Connection reset" error within the aiter_raw() block.
Expected behavior
I expect to see a stack trace with aiter_raw() in it, because aiter_raw() caused an exception. (Sure, response.aclose()also throws an exception; but as a user, do I really care?)
Actual behavior
The stack trace is for the error in response.aclose(). aiter_raw() doesn't appear in it.
As a workaround, this code block will raise the correct exception:
request = client.build_request("GET", f"http://{host}:{port}")
response = await client.send(request, stream=True)
try:
async for blob in response.aiter_raw():
pass
except Exception as err:
try:
await response.aclose()
finally:
raise err
... this uses the inside knowledge that aiter_raw() calls aclose() to handle these three cases:
Exception while streaming and exception from aclose(): ignore the exception from aclose() and raise the one from aiter_raw().
Exception while streaming and success from aclose(): raise the exception from aiter_raw().
Streaming succeeds but exception in aiter_raw()'s call to aclose(): we'll call aclose() again in our exception handler; it will no-op (because it's a second invocation); and we'll raise the exception from the original call to aclose().
I've been using Python for decades, and I feel very uncomfortable nesting exception handlers like this.
@adamhooper I'm not sure yet I fully understood the problem here.
Here's a sample script to try and simulate the situation I think you're referring to:
importhttpximporthttpcoreimporttrioclassBrokenStream:
def__iter__(self):
yieldb""raisehttpcore.ReadError("Stream iterator")
asyncdef__aiter__(self):
yieldb""raisehttpcore.ReadError("Stream iterator")
defclose(self):
raisehttpcore.CloseError("Closing")
asyncdefaclose(self):
raisehttpcore.CloseError("Closing")
defapp(request):
returnhttpx.Response(200, stream=BrokenStream())
transport=httpx.MockTransport(app)
url="https://example.org"# Comment this out to let the async main run.withhttpx.Client(transport=transport) asclient:
withclient.stream("GET", url) asresponse:
for_inresponse.iter_raw():
pass@trio.runasyncdefmain():
asyncwithhttpx.AsyncClient(transport=transport) asclient:
asyncwithclient.stream("GET", url) asresponse:
asyncfor_inresponse.aiter_raw():
pass
The output from this is:
Sync:
Traceback (most recent call last): File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_exceptions.py", line 326, in map_exceptions yield File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_models.py", line 1234, in iter_raw for raw_stream_bytes in self.stream: File "_debug/script.py", line 10, in __iter__ raise httpcore.ReadError("Stream iterator")httpcore.ReadError: Stream iteratorThe above exception was the direct cause of the following exception:Traceback (most recent call last): File "_debug/script.py", line 24, in <module> for _ in response.iter_raw(): File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_models.py", line 1237, in iter_raw yield chunk File "/Users/florimond.manca/.pyenv/versions/3.8.5/lib/python3.8/contextlib.py", line 131, in __exit__ self.gen.throw(type, value, traceback) File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_exceptions.py", line 343, in map_exceptions raise mapped_exc(message, **kwargs) from exc # type: ignorehttpx.ReadError: Stream iteratorDuring handling of the above exception, another exception occurred:Traceback (most recent call last): File "_debug/script.py", line 25, in <module> pass File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_client.py", line 1814, in __exit__ self.response.close() File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_models.py", line 1252, in close self._on_close(self) File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_client.py", line 867, in on_close stream.close() File "_debug/script.py", line 13, in close raise httpcore.CloseError("Closing")httpcore.CloseError: Closing
Async:
Traceback (most recent call last): File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_exceptions.py", line 326, in map_exceptions yield File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_models.py", line 1329, in aiter_raw async for raw_stream_bytes in self.stream: File "_debug/script.py", line 13, in __aiter__ raise httpcore.ReadError("Stream iterator")httpcore.ReadError: Stream iteratorThe above exception was the direct cause of the following exception:Traceback (most recent call last): File "_debug/script.py", line 41, in main async for _ in response.aiter_raw(): File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_models.py", line 1332, in aiter_raw yield chunk File "/Users/florimond.manca/.pyenv/versions/3.8.5/lib/python3.8/contextlib.py", line 131, in __exit__ self.gen.throw(type, value, traceback) File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_exceptions.py", line 343, in map_exceptions raise mapped_exc(message, **kwargs) from exc # type: ignorehttpx.ReadError: Stream iteratorDuring handling of the above exception, another exception occurred:Traceback (most recent call last): File "_debug/script.py", line 38, in <module> async def main(): File "/Users/florimond.manca/Developer/encode-projects/httpx/venv/lib/python3.8/site-packages/trio/_core/_run.py", line 1896, in run raise runner.main_task_outcome.error File "_debug/script.py", line 42, in main pass File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_client.py", line 1836, in __aexit__ await self.response.aclose() File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_models.py", line 1347, in aclose await self._on_close(self) File "/Users/florimond.manca/Developer/encode-projects/httpx/httpx/_client.py", line 1503, in on_close await stream.aclose() File "_debug/script.py", line 19, in aclose raise httpcore.CloseError("Closing")httpcore.CloseError: Closing
We already have #1465 to fix the leaking httpcore.CloseError.
But from as far as I can see, we do see errors from iter_raw() as well as errors from .close(). They're included as causes. Right? What else would you expect to see?
@trio.run
async def main():
async with httpx.AsyncClient(transport=transport) as client:
try:
async with client.stream("GET", url) as response:
async for _ in response.aiter_raw():
pass
except httpx.ReadError:
print("The server broke while we were reading")
except httpx.CloseError:
print("The server broke while we were closing")
I'd expect to catch httpx.ReadError in this case, not httpx.CloseError.
Hmm I see. Well unfortunately Python only follows a "single-exception-at-a-time" design, so you'd need to catch errors as close to where they happen as possible.
So what you could do as a workaround is…
@trio.runasyncdefmain():
asyncwithhttpx.AsyncClient(transport=transport) asclient:
try:
asyncwithclient.stream("GET", url) asresponse:
try:
asyncfor_inresponse.aiter_raw():
passexcepthttpx.ReadError:
print("The server broke while we were reading")
excepthttpx.CloseError:
print("The server broke while we were closing")
Which outputs the following, and doesn't look too bad, does it?
The server broke while we were readingThe server broke while we were closing
Sure -- there's also the workaround I mentioned above.
When should users use the approach laid out in the documentation, and when should users use one of these workarounds that raises the exception users want?
classResponseStream(SyncByteStream):
def__init__(self, httpcore_stream: httpcore.SyncByteStream):
self._httpcore_stream=httpcore_streamdef__iter__(self) ->typing.Iterator[bytes]:
try:
withmap_httpcore_exceptions():
forpartinself._httpcore_stream:
yieldpartexceptExceptionasexc:
# If an exception occurs during streaming the data, then close the stream,# but mask any close exceptions that occur.try:
self._httpcore_stream.close()
exceptException:
passraiseexc
Although it's not immediately clear to me if that's what we ought to be doing, ...or... if we should be ensuring something similar, but inside httpcore.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Activity
florimondmanca commentedon Feb 12, 2021
@adamhooper I'm not sure yet I fully understood the problem here.
Here's a sample script to try and simulate the situation I think you're referring to:
The output from this is:
Sync:
Async:
We already have #1465 to fix the leaking
httpcore.CloseError.But from as far as I can see, we do see errors from
iter_raw()as well as errors from.close(). They're included as causes. Right? What else would you expect to see?adamhooper commentedon Feb 12, 2021
The error comes when you want to catch the error.
I'd expect to catch
httpx.ReadErrorin this case, nothttpx.CloseError.florimondmanca commentedon Feb 12, 2021
Hmm I see. Well unfortunately Python only follows a "single-exception-at-a-time" design, so you'd need to catch errors as close to where they happen as possible.
So what you could do as a workaround is…
Which outputs the following, and doesn't look too bad, does it?
adamhooper commentedon Feb 12, 2021
Sure -- there's also the workaround I mentioned above.
When should users use the approach laid out in the documentation, and when should users use one of these workarounds that raises the exception users want?
euri10 commentedon Feb 12, 2021
if this lands could be helpful
florimondmanca commentedon Feb 16, 2021
@tomchristie Heya :-) Any thoughts on this one, or #1465?
lovelydinosaur commentedon Apr 14, 2021
Yup, we really ought to be handling this neatly.
We could for instance do something like this...
Although it's not immediately clear to me if that's what we ought to be doing, ...or... if we should be ensuring something similar, but inside
httpcore.stale commentedon Feb 20, 2022
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
lovelydinosaur commentedon Feb 21, 2022
Probably still valid thanks @Stale - needs looking into.
stale commentedon Mar 25, 2022
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
stale commentedon Oct 15, 2022
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
lovelydinosaur commentedon Dec 8, 2022
I believe this was closed by encode/httpcore#310