Request hooks and middleware-like logic

Flask · Advanced · 10 min

Authentication checks, timing, headers and cleanup belong in hooks, not copied into every view.

Flask exposes hooks, decorators that register functions to run at points in the request lifecycle:

  • @app.before_request: runs before the view. If it returns a value other than None, that value becomes the response and the view is skipped, perfect for "redirect to login" or rejecting requests early.
  • @app.after_request: receives the response object and must return a response (usually the same one). Use it to add headers. It does not run if an unhandled exception occurred.
  • @app.teardown_request and @app.teardown_appcontext: always run at the end, with the exception (or None) as argument. They cannot change the response; use them for cleanup.

@app.url_value_preprocessor and @app.url_defaults handle shared URL parameters such as a language code.

Blueprints have the same hooks scoped to their routes (@bp.before_request) plus app-wide versions (@bp.before_app_request). Multiple before_request functions run in registration order; after_request functions run in reverse.

For a reusable per-view check, a decorator such as login_required that wraps the view is often clearer than a global hook with a list of exceptions.

Hooks run inside Flask. True WSGI middleware wraps app.wsgi_app and sees every request before Flask does (app.wsgi_app = ProxyFix(app.wsgi_app)); use it for protocol-level concerns, and hooks for application logic.

Example

import time
from flask import Flask, g, request

app = Flask(__name__)

@app.before_request
def start_timer_and_auth():
    g.start = time.perf_counter()
    if request.path.startswith("/admin") and request.headers.get("X-Token") != "s3cret":
        return {"error": "forbidden"}, 403      # short-circuits: view never runs

@app.after_request
def add_headers(response):
    response.headers["X-Elapsed-ms"] = f"{(time.perf_counter() - g.start) * 1000:.1f}"
    return response                             # must return the response

@app.teardown_request
def log_end(exc):
    print("teardown, error:", exc)
@app.get("/admin/stats")
def stats():
    return {"users": 42}
c = app.test_client()
r = c.get("/admin/stats"); print(r.status_code, "X-Elapsed-ms" in r.headers)   # 403 True
print(c.get("/admin/stats", headers={"X-Token": "s3cret"}).json)            # {'users': 42}

Remember

  • before_request returning a value skips the view
  • after_request must return the response and is skipped on unhandled errors
  • teardown_* always runs and is for cleanup

Watch out for

  • Forgetting to return the response from an after_request function.

Check yourself

  1. A before_request function returns ("Login required", 401). 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/api/#flask.Flask.before_request