Async request context isolation missing task-local propagation in Flask 2.3
0 reputation · 01 Sept 2020, 21:13 UTC
0 reputation · 01 Sept 2020, 21:13 UTC
Flask 2.0 introduced async view functions, yet the request context remains bound to thread-local storage via Werkzeug's LocalProxy. When multiple concurrent async requests execute on the same event loop, the request and g proxies can reference the wrong request if a task yields control. The copy_current_request_context decorator, which captures the context for background threads, does not accept async callables, leaving no supported way to propagate the request context into spawned async tasks.
Extensions that access request or session at import time or in middleware assume thread-local safety. Under ASGI deployment with Uvicorn, this assumption breaks because the concurrency model shifts from threads to tasks. No official pattern exists for safely sharing request-bound data across await points or for wrapping sync middleware so it does not leak context into async views.
await boundaries in Flask 2.3 async views?LocalProxy replacement planned, or should applications implement their own contextvar-based wrappers?29775 reputation · 02 Sept 2020, 03:50 UTC
In Flask 2.3, the recommended pattern for preserving request identity across await boundaries is to rely on the built-in integration between Werkzeug's LocalProxy and Python's contextvars. Flask's async support utilizes contextvars to ensure that the request and g proxies are task-local. This means that as long as your code remains within the same asynchronous task chain (including await points), the context is preserved automatically.
A critical distinction exists between awaiting a coroutine and spawning a background task. While await preserves the context, asyncio.create_task() captures the current contextvars context at the moment of creation. Therefore, a task spawned within an async view will have access to the request proxy.
However, if you are spawning tasks that are intended to outlive the request lifecycle (fire-and-forget), relying on the request proxy is unsafe. Once the request finishes and the context is popped, the proxy will reference nothing or raise a RuntimeError. In these cases, you must explicitly pass the required data as arguments:
async def background_worker(user_id, request_id):
# Do not use 'from flask import request' here
await process_data(user_id, request_id)
@app.route("/process")
async def handle_request():
# Pass specific data, not the proxy
asyncio.create_task(background_worker(request.user_id, request.id))
return "Processing"
Writing a single middleware for both sync and async chains is complex because sync middleware typically operates on the WSGI environment, while async views in an ASGI deployment (like Uvicorn) operate on the ASGI scope. To avoid contamination:
contextvars for any custom request-scoped data.app_context rather than assuming a thread-local environment.There is no planned removal of LocalProxy in favor of a pure contextvar wrapper because LocalProxy has already been updated to use contextvars under the hood for async compatibility. Applications should not implement their own contextvar wrappers for standard Flask request data, as this would bypass the framework's internal context management.
To verify your environment is correctly isolating contexts, you can run a concurrency test:
import asyncio
from flask import Flask, request
app = Flask(__name__)
@app.route("/test")
async def test_isolation():
val = request.args.get("id")
await asyncio.sleep(1) # Yield control to other tasks
return f"Request {val} received {request.args.get('id')}"
If multiple concurrent requests return the correct IDs without mixing, contextvars propagation is functioning. One diagnostic detail: Are you using a WSGI server with a worker class that emulates async (like Gunicorn gevent) or a native ASGI server (like Uvicorn)? This changes whether the context is managed by threads or native Python tasks.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.