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_token is already set, it returns, so nested pushes only count.
  • Otherwise it stores the context in the _cv_app context variable, keeping the token in _cv_token, and sends appcontext_pushed.
  • For a request context, it calls _get_session() to open the session first. Then, if url_adapter is not None, it calls match_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:

  1. It reads req = ctx.request. If req.routing_exception is set, it calls raise_routing_exception(req).
  2. It takes rule = req.url_rule. If rule.provide_automatic_options is true and the method is OPTIONS, it returns make_default_options_response(ctx).
  3. 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:

  1. make_response(rv) converts the return value (or an HTTPException) into a Response.
  2. process_response(ctx, response) runs the postprocessing functions.
  3. request_finished is sent with the response.

process_response() applies functions in this order:

  • Functions in ctx._after_request_functions run first.
  • Then, for each name in chain(ctx.request.blueprints, (None,)), the functions in self.after_request_funcs[name] run in reversed() order. The None scope 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 in environ, it is called with ctx.
  • If should_ignore_error is set and returns true for the error, error is reset to None.
  • 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:

  1. If there is a request, app.do_teardown_request(self, exc) and then self._request.close().
  2. app.do_teardown_appcontext(self, exc).
  3. _cv_app.reset(self._cv_token) and _cv_token = None.
  4. The appcontext_popped signal.

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_count is a mutable counter that makes push/pop re-entrant.
  • _cv_token is the _cv_app context-variable token. None means the context is not pushed.
  • _cv_app is a context variable holding the active context.
  • ctx.request.blueprints is the sequence of blueprint names that scopes the handler lookups.
  • ctx._after_request_functions holds per-request functions, such as those registered with after_this_request.
  • Flask._got_first_request is a flag set to True in full_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 exposes ctx.request.
  • Routing input is derived during push() by match_request(). It produces req.url_rule, req.view_args, and possibly req.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 keeps request, session, and g available 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 session should also read it in the view so the Vary: cookie header is set.
    • The generator must not modify the session, because Set-Cookie has already been sent.
    • The documentation recommends passing the needed data as arguments rather than relying on context objects.
@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() calls make_response(rv). A view may return a non-Response value, and it is converted there.
  • Session persistence: save_session() runs in process_response() and writes onto the response, unless the session is a null session.
  • Wire output: response(environ, start_response) returns Iterable[bytes] to the WSGI server.
  • Public API: the flask package re-exports the user-facing pieces, including Flask, Blueprint, request, session, g, current_app, jsonify, render_template, redirect, abort, send_file, and url_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):

  1. For BadRequestKeyError, if debug or TRAP_BAD_REQUEST_ERRORS is set, it sets e.show_exception = True.
  2. If e is an HTTPException and trap_http_exception(e) is false, it returns handle_http_exception(ctx, e).
  3. 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).

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 returns e unchanged.
  • If e is a RoutingException, it returns e unchanged. 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):

  1. It sends got_request_exception.
  2. It reads config["PROPAGATE_EXCEPTIONS"]. If it is None, it defaults to self.testing or self.debug.
  3. If propagating, it re-raises: a bare raise if sys.exc_info()[1] is e, otherwise raise e.
  4. Otherwise it calls log_exception(ctx, exc_info) and wraps the error as InternalServerError(original_exception=e). It looks up a handler for that error and, if one exists, replaces server_error with the handler's result.
  5. 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.