Skip to main content

Managing the Request Lifecycle

The request lifecycle in this application is a strictly ordered sequence of execution phases designed to provide hooks for global behavior, resource management, and response modification. This orchestration is primarily handled by the Flask class in src/flask/app.py, while the registration of lifecycle hooks is managed by the Scaffold base class in src/flask/sansio/scaffold.py.

The Orchestration Layer

The entry point for every request is the wsgi_app method in the Flask class. It manages the lifecycle of the AppContext, ensuring that even if a request fails, resources are cleaned up.

# 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 full_dispatch_request method defines the high-level sequence: pre-processing, dispatching to the view, and finalization (post-processing).

Pre-processing Phase

Pre-processing allows the application to perform setup tasks before the view function is executed. This phase consists of two main types of hooks:

  1. URL Value Preprocessors: Registered via @app.url_value_preprocessor, these functions can modify the dictionary of values captured by the URL rule before they reach the view.
  2. Before Request Hooks: Registered via @app.before_request, these functions run after URL pre-processing.

A critical feature of before_request hooks is their ability to short-circuit the request. If a before_request function returns a non-None value, that value is immediately treated as the response, and the view function is never called.

# src/flask/app.py

def preprocess_request(self, ctx: AppContext) -> ft.ResponseReturnValue | None:
req = ctx.request
names = (None, *reversed(req.blueprints))

for name in names:
if name in self.url_value_preprocessors:
for url_func in self.url_value_preprocessors[name]:
url_func(req.endpoint, req.view_args)

for name in names:
if name in self.before_request_funcs:
for before_func in self.before_request_funcs[name]:
rv = self.ensure_sync(before_func)()

if rv is not None:
return rv

return None

Post-processing Phase

Once a view function (or a short-circuiting pre-processor) returns a value, the application enters the post-processing phase via finalize_request. This phase converts the return value into a proper Response object and runs "after request" hooks.

Response Modification

Functions registered with @app.after_request receive the response object and must return it (or a new one). Unlike pre-processors, these hooks are executed in reverse order of registration.

The application also supports after_this_request, which allows registering a hook dynamically during the execution of a single request (e.g., inside a view). These are executed before the global after_request hooks.

# src/flask/app.py

def process_response(self, ctx: AppContext, response: Response) -> Response:
# 1. Run hooks registered specifically for this request
for func in ctx._after_request_functions:
response = self.ensure_sync(func)(response)

# 2. Run global and blueprint hooks in reverse order
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)
# ...
return response

Teardown and Resource Management

The teardown phase is the most robust part of the lifecycle. Registered via @app.teardown_request, these functions are called when the request context is popped, regardless of whether the request was successful or raised an unhandled exception.

This makes teardown_request the appropriate place for resource cleanup, such as closing database connections or file handles.

Teardown Constraints

  • Exception Handling: Teardown functions are passed an exception object if the request failed. If an error handler already caught the exception, the teardown function receives None.
  • Reliability: The do_teardown_request method in Flask uses a collection mechanism to ensure that even if one teardown function fails, the rest are still executed.
  • No Response Modification: Unlike after_request, the return values of teardown functions are ignored.
# src/flask/app.py

def do_teardown_request(self, ctx: AppContext, exc: BaseException | None = None) -> None:
collect_errors = _CollectErrors()

for name in chain(ctx.request.blueprints, (None,)):
if name in self.teardown_request_funcs:
for func in reversed(self.teardown_request_funcs[name]):
with collect_errors:
self.ensure_sync(func)(exc)
# ...
collect_errors.raise_any("Errors during request teardown")

Contextual Lifecycle Summary

The entire lifecycle is bound to the AppContext (defined in src/flask/ctx.py). In version 3.2 of this codebase, RequestContext has been merged into AppContext. The lifecycle stages correspond to the context's state:

StageTriggerKey Methods
Initializationctx.push()match_request(), session loading
Executionfull_dispatch_request()preprocess_request(), dispatch_request(), process_response()
Cleanupctx.pop()do_teardown_request(), do_teardown_appcontext()

By separating post-processing (after_request) from teardown (teardown_request), the architecture ensures that response-modifying logic only runs on success, while critical cleanup logic runs every time.