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
Response not closed when timeout/cancel reading response stream, which cause RuntimeError: The connection pool was closed while 1 HTTP requests/responses were still in-flight. #2139
Response not closed when timeout/cancel reading response stream, which cause RuntimeError: The connection pool was closed while 1 HTTP requests/responses were still in-flight.
Reproduce:
importasyncioimporthttpxasyncdefmain():
url="https://httpbin.org/drip?delay=0&duration=5"# Or use local httpbin server:# docker run -ti -p 8088:80 kennethreitz/httpbin:latesturl="http://127.0.0.1:8088/drip?delay=0&duration=5"asyncwithhttpx.AsyncClient(timeout=10, trust_env=False) asclient:
try:
coro=client.get(url)
response=awaitasyncio.wait_for(coro, 3)
exceptExceptionasex:
print(type(ex), repr(ex))
else:
print(response)
if__name__=="__main__":
asyncio.run(main())
try:
ifnotstream:
awaitresponse.aread() # will raise CancelledErrorreturnresponseexceptExceptionasexc: # pragma: no coverawaitresponse.aclose() # will not executedraiseexc
Change all except Exception in _client.py to except BaseException can resolve the issue.
I had just investigated an issue which is probably the same bug. I didn't get to your level of investigation / getting into the root cause, but it might still help others 🤷
In my case the canceled tasks were holding slots in the connection pool. I'll also add that the requests that we try to cancel never complete : increasing the size of the connection pool just results in a memory leak and once it is full the client can never be used anymore for the whole runtime.
This is a minimal example canceling a request and trying to send another right away
importasyncioimporthttpxurl="http://perdu.com"client=httpx.AsyncClient(limits=httpx.Limits(max_connections=1))
asyncdefget():
awaitclient.get(url)
asyncdefget_cancel():
task=asyncio.create_task(get())
# the bug does not reproduce if we don't give time for the request to be sentawaitasyncio.sleep(0.001)
task.cancel()
asyncdefmain():
awaitget_cancel()
awaitasyncio.sleep(2) # the request should have finishedawaitget() # !!! httpx.PoolTimeout !!!asyncio.run(main())
EDIT: It might also help to note that the bug appears to have been introduced somewhere between version 0.18 and 0.21 since applying this upgrade appears to have triggered the bug for me.
This seems like something that's affecting several people so let's catch on with this. FYI I am able to reproduce from the script provided in issue description - thanks so much for providing it.
As we can see from the traceback, this actually seems to be an issue that should be resolved in https://github.com/encode/httpcore, which hosts the core networking / connection pool implementation, for multiple async libraries (asyncio, trio).
I'd suggest as a next step we find a reproduction script that only uses HTTPCore, see async usage here: https://www.encode.io/httpcore/async/. Then we can pinpoint what needs fixing in HTTPCore, and after that if there's anything left to fix in HTTPX.
EDIT: It might also help to note that the bug appears to have been introduced somewhere between version 0.18 and 0.21 since applying this upgrade appears to have triggered the bug for me.
IIRC HTTPX 0.20 came with a complete overhaul of HTTPCore (version 0.14.0), which might explain this. We could verify this by bisecting on httpcore==0.14.0
I'd suggest as a next step we find a reproduction script that only uses HTTPCore, see async usage here: https://www.encode.io/httpcore/async/. Then we can pinpoint what needs fixing in HTTPCore, and after that if there's anything left to fix in HTTPX.
$ python ./example.py
<class 'asyncio.exceptions.TimeoutError'>TimeoutError()
Traceback (most recent call last):
File "/Users/tomchristie/Temp/./example.py", line 34, in<module>asyncio.run(main())
File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/runners.py", line 44, in run
return loop.run_until_complete(main)
File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py", line 642, in run_until_complete
returnfuture.result()
File "/Users/tomchristie/Temp/./example.py", line 31, in main
print(response)
File "/Users/tomchristie/Temp/venv/lib/python3.9/site-packages/httpx/_client.py", line 1977, in __aexit__
await self._transport.__aexit__(exc_type, exc_value, traceback)
File "/Users/tomchristie/Temp/venv/lib/python3.9/site-packages/httpx/_transports/default.py", line 332, in __aexit__
await self._pool.__aexit__(exc_type, exc_value, traceback)
File "/Users/tomchristie/Temp/venv/lib/python3.9/site-packages/httpcore/_async/connection_pool.py", line 326, in __aexit__
await self.aclose()
File "/Users/tomchristie/Temp/venv/lib/python3.9/site-packages/httpcore/_async/connection_pool.py", line 312, in aclose
raise RuntimeError(
RuntimeError: The connection pool was closed while 1 HTTP requests/responses were still in-flight.
I haven't properly understood for myself exactly why that happens, but I assume it's because in the second case the cancellation exception is occurring within httpx, not httpcore.
I believe that we already have this fixed within httpcore, and that the proposed fix here is also necessary to ensure correct response closing at the httpx level too.
So before we aclose() the pool when exiting the context manager, the response gets properly closed thanks to the finally: await response.aclose().
On the other hand, HTTPX does not call .request() directly, instead it calls into .handle_async_request() and manages aread/aclose via the Response class, which does leave it in an "open" state when inside a simple timeout as we have here:
Which might hide a similar bug at connect time (here we only reproduce against reads).
I'm still not sure if we should prefer BaseException, enclosing both cancellations as well as SystemExit etc, or a narrower except (backend.cancellation_exc_class, Exception).
even though this issue is marked as closed, I still suffer from it. while trying to track it down, I noticed that the issue happens when asyncio.CancelledError is raised before the try/except statement here: https://github.com/encode/httpcore/blob/master/httpcore/_async/connection.py#L62-L86. On my local tests, I managed to fix this issue by surrounding the whole content under async with self._request_lock with the try block, catching BaseException instead of Exception. I can open a PR if anyone is interested, but I'll need help writing a proper test to it
Activity
remi-dupre commentedon Mar 22, 2022
Hi!
I had just investigated an issue which is probably the same bug. I didn't get to your level of investigation / getting into the root cause, but it might still help others 🤷
In my case the canceled tasks were holding slots in the connection pool. I'll also add that the requests that we try to cancel never complete : increasing the size of the connection pool just results in a memory leak and once it is full the client can never be used anymore for the whole runtime.
This is a minimal example canceling a request and trying to send another right away
EDIT: It might also help to note that the bug appears to have been introduced somewhere between version 0.18 and 0.21 since applying this upgrade appears to have triggered the bug for me.
instant_answer: remove cancels on external API
instant_answer: remove cancels on external API (#323)
guyskk commentedon Mar 23, 2022
@remi-dupre I can reproduce your case, but it seems multiple reason cause your issue.
connect_failedflag not set because not catch BaseException: https://github.com/encode/httpcore/blob/master/httpcore/_async/connection.py#L84except Exceptionin httpcore which probably bugs too, which may cause other issues.We need help from httpx maintainer, it's a big problem.
guyskk commentedon Mar 28, 2022
Hi @florimondmanca, I submit a PR #2148 to resolve issue, could you help review?
The PR not fix case of @remi-dupre, which seems another issue and need a bigger PR to fix.
florimondmanca commentedon Mar 28, 2022
Hi all,
This seems like something that's affecting several people so let's catch on with this. FYI I am able to reproduce from the script provided in issue description - thanks so much for providing it.
As we can see from the traceback, this actually seems to be an issue that should be resolved in https://github.com/encode/httpcore, which hosts the core networking / connection pool implementation, for multiple async libraries (asyncio, trio).
I'd suggest as a next step we find a reproduction script that only uses HTTPCore, see async usage here: https://www.encode.io/httpcore/async/. Then we can pinpoint what needs fixing in HTTPCore, and after that if there's anything left to fix in HTTPX.
IIRC HTTPX 0.20 came with a complete overhaul of HTTPCore (version 0.14.0), which might explain this. We could verify this by bisecting on httpcore==0.14.0
lovelydinosaur commentedon Mar 28, 2022
Fantastic point @florimondmanca, yup.
Following on from your suggestion, I took a look at that just now...
Which results in...
In constrast to running essentially the same thing with
httpx...Which replicates the issue as described...
I haven't properly understood for myself exactly why that happens, but I assume it's because in the second case the cancellation exception is occurring within
httpx, nothttpcore.I believe that we already have this fixed within
httpcore, and that the proposed fix here is also necessary to ensure correct response closing at thehttpxlevel too.florimondmanca commentedon Mar 28, 2022
@tomchristie We're investigating this concurrently :-)
I came to the same conclusion that the default usage with HTTPCore's
.request()does not reproduce the error.This is because it is a shorthand that calls all of aread/aclose as necessary: https://github.com/encode/httpcore/blob/44b8eedfe3bc3c4b15d54810c36f05e7c5fd600d/httpcore/_async/interfaces.py#L17-L46
This snippet is most relevant:
So before we
aclose()the pool when exiting the context manager, the response gets properly closed thanks to thefinally: await response.aclose().On the other hand, HTTPX does not call
.request()directly, instead it calls into.handle_async_request()and manages aread/aclose via theResponseclass, which does leave it in an "open" state when inside a simple timeout as we have here:httpx/httpx/_client.py
Lines 1697 to 1698 in d07c4b4
So the issue is indeed on the HTTPX side.
Essentially, we need to move
await response.aclose()into afinallyblock, which mimicks what HTTPCore does in its.request()helper method.florimondmanca commentedon Mar 28, 2022
Actually, there might be some changes needed on the HTTPCore side too.
I think about usages such as this one:
https://github.com/encode/httpcore/blob/44b8eedfe3bc3c4b15d54810c36f05e7c5fd600d/httpcore/_async/connection.py#L84-L86
Which might hide a similar bug at connect time (here we only reproduce against reads).
I'm still not sure if we should prefer
BaseException, enclosing both cancellations as well asSystemExitetc, or a narrowerexcept (backend.cancellation_exc_class, Exception).lovelydinosaur commentedon Mar 28, 2022
Agreed. I can see there's a number of places where we clean up resources within an
except ... close ... re-raiseconstruct.I think they essentially all ought to be
BaseException.rafaelfelix commentedon Sep 8, 2022
even though this issue is marked as closed, I still suffer from it. while trying to track it down, I noticed that the issue happens when
asyncio.CancelledErroris raised before the try/except statement here: https://github.com/encode/httpcore/blob/master/httpcore/_async/connection.py#L62-L86. On my local tests, I managed to fix this issue by surrounding the whole content underasync with self._request_lockwith the try block, catchingBaseExceptioninstead ofException. I can open a PR if anyone is interested, but I'll need help writing a proper test to it