Understanding AsyncExitStack Middleware
In FastAPI, managing the lifecycle of asynchronous resources—such as temporary files from form uploads—requires a mechanism that ensures cleanup occurs after a request is fully processed, including any streaming responses. This is handled by the AsyncExitStackMiddleware, which leverages Python's contextlib.AsyncExitStack to provide a request-scoped cleanup container.
The Role of AsyncExitStackMiddleware
The AsyncExitStackMiddleware (found in fastapi/middleware/asyncexitstack.py) is a standard ASGI middleware. Its primary responsibility is to initialize an AsyncExitStack at the start of a request and ensure it is closed after the response has been sent.
The implementation is straightforward:
class AsyncExitStackMiddleware:
def __init__(
self, app: ASGIApp, context_name: str = "fastapi_middleware_astack"
) -> None:
self.app = app
self.context_name = context_name
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
async with AsyncExitStack() as stack:
scope[self.context_name] = stack
await self.app(scope, receive, send)
By wrapping the call to self.app in an async with block, FastAPI guarantees that any callbacks or context managers pushed onto the stack will be executed when the request lifecycle ends, even if an exception occurs during processing.
Resource Cleanup in the Request Lifecycle
The most common use case for this middleware in FastAPI is the automatic closing of uploaded files. When a route expects form data or files, the routing logic in fastapi/routing.py retrieves the stack from the ASGI scope and registers a cleanup callback.
In fastapi/routing.py, the get_request_handler function creates an internal app function that manages this:
# In fastapi/routing.py
async def app(request: Request) -> Response:
# ...
file_stack = request.scope.get("fastapi_middleware_astack")
assert isinstance(file_stack, AsyncExitStack), (
"fastapi_middleware_astack not found in request scope"
)
# ...
if is_body_form:
body = await request.form()
# Register the form's close method to be called by the middleware stack
file_stack.push_async_callback(body.close)
When body.close() is pushed to the stack, it ensures that any temporary files created by Starlette while parsing the multipart form data are deleted once the request is finished.
Middleware Stack Placement
The placement of AsyncExitStackMiddleware within the application's middleware stack is critical. In fastapi/applications.py, the build_middleware_stack method explicitly places it inside user-defined middlewares:
# In fastapi/applications.py
def build_middleware_stack(self) -> ASGIApp:
# ...
middleware = (
[Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
+ self.user_middleware
+ [
Middleware(ExceptionMiddleware, handlers=exception_handlers, debug=debug),
# Add FastAPI-specific AsyncExitStackMiddleware for closing files.
Middleware(AsyncExitStackMiddleware),
]
)
# ...
This specific ordering serves two purposes:
- Context Persistence: By being inside user middlewares, the
AsyncExitStackoperates within the samecontextvarscontext as the rest of the application logic. This was historically important when the stack also managed dependencies withyield. - Cleanup Timing: It ensures that resources are cleaned up after the response is generated but before the outer exception-handling middlewares finish their execution.
Comparison of Request Stacks
FastAPI distinguishes between different types of resource lifecycles by using multiple AsyncExitStack instances stored in the ASGI scope:
| Scope Key | Managed By | Primary Purpose |
|---|---|---|
fastapi_middleware_astack | AsyncExitStackMiddleware | Closing uploaded files and form data. |
fastapi_inner_astack | routing.py logic | Managing request-scoped dependencies that use yield. |
fastapi_function_astack | Dependency solver | Managing function-scoped dependencies within a single sub-dependency tree. |
While fastapi_middleware_astack provides the outermost safety net for the entire request, the fastapi_inner_astack is used specifically for dependencies to better support streaming responses and complex exception handling within yield blocks. This separation prevents issues where a dependency's cleanup logic might interfere with the middleware's ability to finalize the request.