#37385 closed Cleanup/optimization (invalid)
ASGI client disconnects log "CancelledError exception in shielded future" on Python 3.14+
| Reported by: | Natalia Bidart | Owned by: | Carlton Gibson |
|---|---|---|---|
| Component: | HTTP handling | Version: | dev |
| Severity: | Normal | Keywords: | async |
| Cc: | Carlton Gibson | Triage Stage: | Unreviewed |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Since a013c821ea (#37177), built-in middleware are sync-only, so under ASGI the default middleware chain runs in a thread and calls the view back through async_to_sync(). On Python 3.14+, a client disconnecting while the view is still running makes the asyncio logger emit an ERROR with a traceback:
CancelledError exception in shielded future
...
File ".../asgiref/sync.py", line 526, in __call__
ret = await asyncio.shield(exec_coro)
asyncio.exceptions.CancelledError
The request itself is handled correctly. Python 3.14 changed asyncio.shield() to log the shielded future's exception if the outer task was cancelled, and asgiref's SyncToAsync shields the executor future, which ends with CancelledError when the handler cancels the request.
To reproduce, in a startproject project with the default MIDDLEWARE, add:
async def slow_async(request): await asyncio.sleep(3) return HttpResponse("ok") def slow_sync(request): time.sleep(3) return HttpResponse("ok") urlpatterns += [ path("slow-async/", slow_async), path("slow-sync/", slow_sync), ]
Serve it with an async server (uvicorn mysite.asgi:application) and run curl --max-time 1 http://127.0.0.1:8000/slow-async/ (or /slow-sync/). The traceback is logged once the view finishes. The asgi tests also show it (./runtests.py asgi --parallel=1), though they pass.
Change History (6)
comment:1 by , 3 weeks ago
| Cc: | added |
|---|
comment:2 by , 3 weeks ago
| Keywords: | async added |
|---|
comment:3 by , 3 weeks ago
| Owner: | set to |
|---|---|
| Status: | new → assigned |
OK, thanks. I’ll take this. Not sure of the resolution yet.
Ultimately (quick search!) several people seem to be hitting this, and it looks like a regression in CPython
https://github.com/python/cpython/issues/156321
I need to dig in further, input upstream, and assess.
Given that this pre-exists #37177, I don’t believe it’s correct to classify as a Release Blocker.
comment:4 by , 3 weeks ago
One solution could be to close this ticket as wontfix and instead...
Yeah, I think this is invalid for Django itself, independently of whether we can get it fixed in CPython, or work around it in asgiref.
comment:5 by , 3 weeks ago
| Resolution: | → invalid |
|---|---|
| Severity: | Release blocker → Normal |
| Status: | assigned → closed |
Thanks Carlton, I appreciate your feedback!
comment:6 by , 3 weeks ago
Just to close the loop, this will be resolved in CPython, and is marked for backport to 3.14 and 3.15 branches.
One solution could be to close this ticket as
wontfixand instead changeasgirefto not useasyncio.shield(). Since Python 3.14,shield()logs the inner future's exception when the outer task is cancelled, which is what SyncToAsync.__call__ hits here. #37177 only made the default middleware hit it; older Django versions likestable/6.1.xshow the same log with any sync-only middleware.asyncio.wait()protects the executor future the same way without logging, butshield()was also implicitly marking the inner exception as retrieved, so I think theexceptbranch needs to do that explicitly orasynciologsFuture exception was never retrievedinstead:The downside is that if the sync function fails with a real error after the request was cancelled, that error is now silently discarded, as it was before Python 3.14; with
shield()on 3.14+ it would be logged. If that matters, theexceptbranch could still log anything other thanCancelledError. These design decisions are outside my comfort zone so perhaps we should move to anasgirefissue. Carlton, what do you think?