How request and current_app proxies work

Flask · Expert · 12 min

Understanding the proxies explains context errors, thread bugs and when you need the real object.

request, session, g and current_app look like module globals, yet each concurrent request sees its own values. In Flask 3, the active contexts are stored in `contextvars.ContextVar`s, and the names you import are Werkzeug `LocalProxy` objects that look up the current value on every attribute access.

Consequences:

  • Each thread (and each asyncio task) has its own context variable values, so concurrent requests in a threaded server never see each other's request.
  • Accessing a proxy with no active context raises RuntimeError.
  • A proxy is not the object itself: isinstance(request, Request) is True through the proxy, but type(request) is LocalProxy. When you need the real object, for example to pass it to a signal receiver or another thread, call request._get_current_object() (or current_app._get_current_object()).

New threads do not inherit the context. A thread started from a view begins with empty context variables, so request there fails. Options:

  • pass plain data (IDs, dicts) into the thread, the cleanest choice
  • push a context in the thread with with app.app_context():, using the real app object
  • decorate the function with @copy_current_request_context

Contexts form a stack conceptually: pushing an app context inside another (as in nested test clients) shadows the outer one until it is popped. Always use with blocks so they are popped even on errors, otherwise state leaks into later code.

This keeps view signatures clean at the cost of implicit state: keep proxies at the edges and pass explicit values into your core logic.

Example

import threading
from flask import Flask, copy_current_request_context, current_app, request

app = Flask(__name__)
results = []

@app.get("/work")
def work():
    real_app = current_app._get_current_object()
    path = request.path                           # pass plain data into threads

    def plain():
        try: request.path
        except RuntimeError: results.append("no request context in thread")
        with real_app.app_context():
            results.append(f"app ok, path {path}")
    @copy_current_request_context
    def copied():
        results.append(f"copied: {request.path}")
    for fn in (plain, copied):
        t = threading.Thread(target=fn); t.start(); t.join()
    return {"proxy_type": type(request).__name__}
print(app.test_client().get("/work").json)   # {'proxy_type': 'LocalProxy'}
print(*results, sep="\n")
# no request context in thread / app ok, path /work / copied: /work

Remember

  • Flask globals are LocalProxy objects reading contextvars
  • New threads start without a context; pass data or push one
  • _get_current_object() returns the real object behind a proxy

Watch out for

  • Passing the request proxy into a background thread and reading it after the request ended.

Check yourself

  1. A view starts a threading.Thread whose function reads request.args. What happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: flask.palletsprojects.com/en/stable/reqcontext/