Data Flow
Data Processing Flow
A request moves through flask.app and flask.ctx in the stages below. Each stage is a separate method, so subclasses can override one stage without touching the others.
1. WSGI entry point
Flask.__call__() only delegates to Flask.wsgi_app(environ, start_response). The real logic lives in wsgi_app() so that middleware can wrap wsgi_app without losing the reference to the app object.
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
2. Context creation and push
wsgi_app() builds a context from the raw environ with self.request_context(environ) and calls ctx.push(). AppContext.push() in flask.ctx does the following:
- It increments
_push_count. If_cv_tokenis already set, it returns, so nested pushes only count. - Otherwise it stores the context in the
_cv_appcontext variable, keeping the token in_cv_token, and sendsappcontext_pushed. - For a request context, it calls
_get_session()to open the session first. Then, ifurl_adapteris notNone, it callsmatch_request().
The order is deliberate. The session is opened before routing so that custom open_session implementations can use the request context, and so that custom URL converters can read the session.
3. Dispatch
Flask.full_dispatch_request(ctx) runs the main pipeline:
request_started.send(self, _async_wrapper=self.ensure_sync)
rv = self.preprocess_request(ctx)
if rv is None:
rv = self.dispatch_request(ctx)
If preprocess_request() returns a value, dispatch_request() is skipped and that value becomes the response value.
dispatch_request() then does the following:
- It reads
req = ctx.request. Ifreq.routing_exceptionis set, it callsraise_routing_exception(req). - It takes
rule = req.url_rule. Ifrule.provide_automatic_optionsis true and the method isOPTIONS, it returnsmake_default_options_response(ctx). - Otherwise it calls the view with the URL parameters as keyword arguments:
self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args).
The view's return value does not have to be a response object. It is normalized in the next stage.
4. Finalization
full_dispatch_request() always ends with finalize_request(ctx, rv), which does the following:
make_response(rv)converts the return value (or anHTTPException) into aResponse.process_response(ctx, response)runs the postprocessing functions.request_finishedis sent with the response.
process_response() applies functions in this order:
- Functions in
ctx._after_request_functionsrun first. - Then, for each name in
chain(ctx.request.blueprints, (None,)), the functions inself.after_request_funcs[name]run inreversed()order. TheNonescope holds app-wide functions. - Finally, unless
session_interface.is_null_session(ctx._get_session()),session_interface.save_session()writes the session onto the response.
Each function receives the response and its return value becomes the new response, so the data is transformed step by step.
5. Sending and teardown
Back in wsgi_app(), response(environ, start_response) produces the iterable of bytes returned to the server. The finally block then runs:
- If
"werkzeug.debug.preserve_context"is inenviron, it is called withctx. - If
should_ignore_erroris set and returns true for the error,erroris reset toNone. ctx.pop(error)is called.
AppContext.pop(exc) decrements _push_count and returns early while it is still above zero. On the final pop it runs the teardown sequence:
- If there is a request,
app.do_teardown_request(self, exc)and thenself._request.close(). app.do_teardown_appcontext(self, exc)._cv_app.reset(self._cv_token)and_cv_token = None.- The
appcontext_poppedsignal.
do_teardown_request() calls each function in teardown_request_funcs for the request's blueprints and then for None, in reversed registration order, passing exc. It then sends request_tearing_down. do_teardown_appcontext() does the same with teardown_appcontext_funcs and sends appcontext_tearing_down.
Key Data Structures
Handler registries (flask.sansio.scaffold)
The scaffold's __init__ creates these registries as defaultdict(list):
| Attribute | Contents | Used at |
|---|---|---|
before_request_funcs |
{scope: [functions]} run at the start of each request |
Registered via before_request |
after_request_funcs |
{scope: [functions]} run at the end of each request |
process_response() |
teardown_request_funcs |
{scope: [functions]} run even if an exception is raised |
do_teardown_request() |
The scope key is a blueprint name, or None for all requests. The attribute comments mark these structures as internal: they should not be modified directly and their format may change. Functions are meant to be registered through the decorators (before_request, after_request, teardown_request).
Context state (flask.ctx)
_push_countis a mutable counter that makes push/pop re-entrant._cv_tokenis the_cv_appcontext-variable token.Nonemeans the context is not pushed._cv_appis a context variable holding the active context.ctx.request.blueprintsis the sequence of blueprint names that scopes the handler lookups.ctx._after_request_functionsholds per-request functions, such as those registered withafter_this_request.Flask._got_first_requestis a flag set toTrueinfull_dispatch_request().
Signals (flask.signals, re-exported from flask)
Signals fire at lifecycle points: appcontext_pushed, request_started, request_finished, got_request_exception, request_tearing_down, appcontext_tearing_down, and appcontext_popped. They are sent with _async_wrapper=self.ensure_sync. message_flashed, template_rendered, and before_render_template are also exported.
Tagged JSON (flask.json.tag)
Tag classes subclass JSONTag and define check(), to_json() and, for tagged types, to_python(). They convert values that JSON cannot represent:
| Class | key |
Matches | JSON form |
|---|---|---|---|
PassDict |
none | dict |
values tagged recursively; keys untouched, since JSON keys must be strings |
PassList |
none | list |
each item tagged |
TagTuple |
" t" |
tuple |
list of tagged items; to_python() rebuilds a tuple |
TagBytes |
" b" |
bytes |
b64encode(value).decode("ascii"); to_python() uses b64decode |
TagMarkup |
" m" |
objects with a callable __html__ |
str(value.__html__()); always deserializes to Markup |
TagUUID |
" u" |
UUID |
(check() uses isinstance(value, UUID)) |
PassDict and PassList set tag = to_json, so they pass structure through and recurse into children via self.serializer.tag(...). All of these classes declare __slots__ = ().
Input/Output Handling
Input
- Raw input is the WSGI
environ.request_context(environ)wraps it in a context that exposesctx.request. - Routing input is derived during
push()bymatch_request(). It producesreq.url_rule,req.view_args, and possiblyreq.routing_exception.dispatch_request()consumes these. - Session input is loaded in
push()via_get_session()before routing. - Streaming responses:
flask.helpers.stream_with_context()wraps a generator, either as a decorator or around an already-created generator. It keepsrequest,session, andgavailable while the generator runs, even though the request context has typically ended by then.- Headers cannot be sent after the body starts. A generator that reads
sessionshould also read it in the view so theVary: cookieheader is set. - The generator must not modify the session, because
Set-Cookiehas already been sent. - The documentation recommends passing the needed data as arguments rather than relying on context objects.
- Headers cannot be sent after the body starts. A generator that reads
@app.get("/stream")
def streamed_response():
@stream_with_context
def generate():
yield "Hello "
yield request.args["name"]
yield "!"
return Response(generate())
Output
- Normalization:
finalize_request()callsmake_response(rv). A view may return a non-Responsevalue, and it is converted there. - Session persistence:
save_session()runs inprocess_response()and writes onto the response, unless the session is a null session. - Wire output:
response(environ, start_response)returnsIterable[bytes]to the WSGI server. - Public API: the
flaskpackage re-exports the user-facing pieces, includingFlask,Blueprint,request,session,g,current_app,jsonify,render_template,redirect,abort,send_file, andurl_for.
Error Handling
Errors are handled at several layers, each with a different recovery policy.
Layer 1: full_dispatch_request() and handle_user_exception()
Any Exception from preprocess_request() or dispatch_request() goes to handle_user_exception(ctx, e):
- For
BadRequestKeyError, ifdebugorTRAP_BAD_REQUEST_ERRORSis set, it setse.show_exception = True. - If
eis anHTTPExceptionandtrap_http_exception(e)is false, it returnshandle_http_exception(ctx, e). - Otherwise it calls
_find_error_handler(e, ctx.request.blueprints).- With no handler, it re-raises the original exception with its traceback (bare
raise). - With a handler, it returns
self.ensure_sync(handler)(e).
- With no handler, it re-raises the original exception with its traceback (bare
The returned value then goes through finalize_request() like any view result, so errors become responses by the same path.
Layer 2: handle_http_exception()
- If
e.code is None(proxy exceptions), it returnseunchanged. - If
eis aRoutingException, it returnseunchanged. These are used internally, for example for slash redirects, and are not handled in user code. - It looks up a handler. With none, it returns the exception itself, which serves as the response. Otherwise it returns
ensure_sync(handler)(e).
Layer 3: handle_exception()
When wsgi_app() catches an Exception that escaped the layers above (including one raised from an error handler), it calls handle_exception(ctx, e):
- It sends
got_request_exception. - It reads
config["PROPAGATE_EXCEPTIONS"]. If it isNone, it defaults toself.testing or self.debug. - If propagating, it re-raises: a bare
raiseifsys.exc_info()[1] is e, otherwiseraise e. - Otherwise it calls
log_exception(ctx, exc_info)and wraps the error asInternalServerError(original_exception=e). It looks up a handler for that error and, if one exists, replacesserver_errorwith the handler's result. - It returns
finalize_request(ctx, server_error, from_error_handler=True).
server_error = InternalServerError(original_exception=e)
handler = self._find_error_handler(server_error, ctx.request.blueprints)
if handler is not None:
server_error = self.ensure_sync(handler)(server_error)
return self.finalize_request(ctx, server_error, from_error_handler=True)
Finalization failures
In finalize_request(), an exception in process_response() or the request_finished signal is re-raised for normal dispatch. When from_error_handler=True, it is logged with self.logger.exception("Request finalizing failed with an error while handling an error") and the response is still returned. This prevents a failure in an already-failing path from masking the original error.
Non-Exception failures
In wsgi_app(), a bare except: (for example KeyboardInterrupt) records error = sys.exc_info()[1] and re-raises. The finally block still runs, so ctx.pop(error) and teardown happen.
Teardown errors
AppContext.pop(), do_teardown_request(), and do_teardown_appcontext() use _CollectErrors. Each teardown step runs inside with collect_errors:, so a failing teardown function does not stop the remaining ones. After all steps have run, collect_errors.raise_any(...) raises with the messages "Errors during context teardown", "Errors during request teardown", or "Errors during app teardown". This guarantees that _cv_app.reset() and the pop signals run even when a teardown handler fails.
pop() also validates its state and raises RuntimeError if the context is not pushed, if there is no active context, or if the active context is a different one.
Error propagation to teardown
The error captured in wsgi_app() is passed to ctx.pop(error) and on to every teardown function and teardown signal as exc. should_ignore_error can clear it first. In full_dispatch_request(), using should_ignore_error triggers a DeprecationWarning (removal planned in Flask 3.3) that tells users to handle errors in teardown handlers instead.
Debugging aid
flask.debughelpers.explain_template_loading_attempts is documented as helping developers understand what failed when a template is not found.