Skip to main content

Request Lifecycle and Middleware

When you deploy a Flask application, the WSGI server (like Gunicorn or uWSGI) interacts with the Flask object as a callable. Understanding how a request travels from this entry point through the internal dispatching pipeline is essential for building extensions, debugging performance, and implementing custom middleware.

The WSGI Entry Point and Middleware

The Flask class implements the WSGI interface via its __call__ method, which delegates all work to wsgi_app. This separation is a deliberate design choice in Flask to support middleware integration without losing access to the application instance.

# src/flask/app.py

def __call__(self, environ: WSGIEnvironment, start_response: StartResponse):
"""The WSGI server calls the Flask application object as the
WSGI application. This calls :meth:`wsgi_app`, which can be
wrapped to apply middleware.
"""
return self.wsgi_app(environ, start_response)

To apply middleware, you should wrap the wsgi_app method rather than the app object itself. This ensures that the app variable still refers to the Flask instance, allowing you to continue calling methods like app.route or app.config after the middleware is applied.

app = Flask(__name__)
app.wsgi_app = MyMiddleware(app.wsgi_app)

Context Lifecycle Management

Every request in Flask is wrapped in an AppContext. Starting in Flask 3.2, RequestContext has been merged into AppContext, providing a unified container for application-level data (like g and current_app) and request-level data (like request and session).

The wsgi_app method manages the lifecycle of this context using a try...finally block to ensure cleanup occurs even if the request fails:

  1. Creation: ctx = self.request_context(environ) creates the context.
  2. Activation: ctx.push() makes the context active, binding it to the current execution environment (using contextvars).
  3. Deactivation: ctx.pop(error) is called in the finally block, triggering teardown functions and clearing the context.
# src/flask/app.py

def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ)
error = None
try:
try:
ctx.push()
response = self.full_dispatch_request(ctx)
except Exception as e:
error = e
response = self.handle_exception(ctx, e)
return response(environ, start_response)
finally:
ctx.pop(error)

The Request Dispatching Pipeline

The high-level flow of a request is handled by full_dispatch_request. This method coordinates preprocessing, view execution, and response finalization.

Preprocessing

Before the view function runs, Flask executes preprocess_request. This method calls:

  • url_value_preprocessors: Functions that can modify request.view_args before they are passed to the view.
  • before_request_funcs: Functions registered with @app.before_request.

If any before_request function returns a value other than None, Flask stops further processing and treats that value as the response, skipping the view function entirely.

Dispatching

If no preprocessor returned a response, dispatch_request is called. It performs the following:

  1. Checks for routing exceptions (like 404 or 405) encountered during ctx.push().
  2. Handles OPTIONS requests automatically if configured.
  3. Executes the view function associated with the matched endpoint, passing any URL variables as keyword arguments.
# src/flask/app.py

def dispatch_request(self, ctx: AppContext) -> ft.ResponseReturnValue:
req = ctx.request
if req.routing_exception is not None:
self.raise_routing_exception(req)

rule = req.url_rule
# ... handle automatic OPTIONS ...

view_args = req.view_args
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)

Response Finalization and Post-processing

Once a view function returns, the result must be converted into a proper Response object and passed through post-processing hooks. This happens in finalize_request.

Response Conversion

The make_response method is responsible for converting various return types into a flask.Response instance. It supports:

  • Strings/Bytes: Become the response body.
  • Dictionaries/Lists: Automatically converted to JSON via self.json.response.
  • Tuples: Can be (body, status, headers), (body, status), or (body, headers).
  • WSGI Callables: Coerced into a response object.

Post-processing

After the response object is created, process_response executes:

  1. Functions registered via after_this_request (one-time hooks).
  2. Functions registered with @app.after_request (in reverse order of registration).
  3. Session saving via the session_interface.
# src/flask/app.py

def process_response(self, ctx: AppContext, response: Response) -> Response:
for func in ctx._after_request_functions:
response = self.ensure_sync(func)(response)

for name in chain(ctx.request.blueprints, (None,)):
if name in self.after_request_funcs:
for func in reversed(self.after_request_funcs[name]):
response = self.ensure_sync(func)(response)

if not self.session_interface.is_null_session(ctx._get_session()):
self.session_interface.save_session(self, ctx._get_session(), response)

return response

Teardown and Cleanup

The final stage of the lifecycle occurs when ctx.pop() is called. This triggers the "teardown" phase, which is distinct from the "after request" phase because it is guaranteed to run even if an unhandled exception occurred during dispatching.

Flask executes:

  1. do_teardown_request: Calls functions registered with @app.teardown_request.
  2. do_teardown_appcontext: Calls functions registered with @app.teardown_appcontext.

These functions receive the exception object if one was raised, making them the ideal place for closing database connections or releasing file handles. Unlike after_request hooks, teardown functions cannot modify the response as it has already been sent to the WSGI server.