Replies: 4 comments 1 reply
-
|
You're right this is a footgun. The issue is that when you call request.body(), request.json(), request.form(), or request.stream() from a sync route using anyio.run() or asyncio.run(), you create a new event loop in the worker thread. But the Request object's async primitives (like asyncio.Event) are tied to the main event loop. The two loops never communicate, causing indefinite hangs when chunked/slow requests come in. Try this: import anyio
@app.post("/sync")
def some_route(request: Request):
#
data = anyio.from_thread.run(request.json)
# or
body = anyio.from_thread.run(request.body)Here anyio.from_thread.run() delegates the async call back to the main event loop where FastAPI is running instead of creating a new one. So try this maybe this will solve your problem. |
Beta Was this translation helpful? Give feedback.
-
I agree we should describe this in docs |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
First Check
Commit to Help
Example Code
Description
I came across this issue in a real-world code. It stayed hidden for quite some time and it suddenly blew up when one app started to be under sustained load.
The following
Requestmethods:read from underlying stream. Calling such method from a sync route with use of
anyio.runcan cause the whole thread to get indefinitely stuck.It happens because
anyio.run()creates a NEW event loop in the worker thread, separate from the main event loop. The Request stream uses asyncio synchronization primitives (like asyncio.Event) that are bound to the MAIN event loop. When the worker thread's event loop awaitsmessage_event.wait(), it's waiting for an event that will be set in a DIFFERENT event loop, so they never communicate.It is nasty, because it "sometimes works". If at the time of such call whole body has already been read by main thread's event loop - all works great. But it is not always the case - if payload is too large to be read at once or connection is slow or traffic is large and keeps main event loop busy, then our worker thread will await
message_event.wait()- event which is set in a different thread's event loop. It makes worker thread wait for an event that will never happen in it's world.I am (now) aware that it is a misuse of anyio. In this case one should use
anyio.from_thread.runinstead ofanyio.runto delegate call of async function to main thread's event loop instead of creating a new, local one.But that requires deeper knowledge of how FastAPI uses anyio to handle sync routes.
A web developer may want to do something like this:
Of course that won't work, so a dev will ask google/chatgpt/claude how to call async method from sync code. He will probably get a suggestion to use
anyio.runorasyncio.run. That will work in tests, probably will work on dev, but has potential to blow up on production under load.I think it would be good to either document how to call async code from sync routes properly or to provide a utility for that which would abstract use of
anyioaway. Without that it is easy to shoot oneself in the foot.Operating System
macOS
Operating System Details
No response
FastAPI Version
0.128.6
Pydantic Version
2.12.5
Python Version
3.13
Additional Context
No response
Beta Was this translation helpful? Give feedback.
All reactions