Middleware — Code Around Every Request
Intermediate@app.middleware("http") wraps every request: work before call_next, work after — request IDs, timing, access logs. Built-ins cover gzip and host checks; know when a dependency is the better tool.
Overview
Middleware wraps the entire application: every request passes through on the way in, every response on the way out — the natural home for cross-cutting concerns that apply to all routes: request IDs for tracing, timing headers, access logging, compression. FastAPI's @app.middleware("http") gives you the request and a call_next function; everything before the await is inbound, everything after is outbound. Middleware stacks like an onion — added last runs first inbound — and ships with useful built-ins: GZipMiddleware, TrustedHostMiddleware, HTTPSRedirectMiddleware. The design question interviewers probe: middleware vs dependency. Middleware sees every request but knows nothing about which endpoint or user; dependencies are per-route, typed, and overridable in tests. All-routes plumbing → middleware; anything conditional or endpoint-aware → dependency.
Request ID + Timing — the Canonical Middleware
Generate (or propagate) an X-Request-ID, time the handler, log one structured access line, and return both as headers. Every service you build ends up with a version of this exact function.
import logging, time, uuid
from fastapi import FastAPI, Request
app = FastAPI()
log = logging.getLogger("access")
@app.middleware("http")
async def observability(request: Request, call_next):
# ── inbound: before the route (and its dependencies) run ──
request_id = request.headers.get("x-request-id", uuid.uuid4().hex[:12])
request.state.request_id = request_id # visible to endpoints & handlers
start = time.perf_counter()
response = await call_next(request) # ← the entire app runs here
# ── outbound: response exists, decorate + log ──
elapsed_ms = (time.perf_counter() - start) * 1000
response.headers["X-Request-ID"] = request_id
response.headers["X-Response-Time"] = f"{elapsed_ms:.1f}ms"
log.info("%s %s → %d in %.1fms id=%s",
request.method, request.url.path,
response.status_code, elapsed_ms, request_id)
return response
@app.get("/orders/{order_id}")
def get_order(order_id: int, request: Request):
# the id set by middleware, usable in logs / error reports:
return {"order": order_id, "trace": request.state.request_id}
# access log: GET /orders/42 → 200 in 3.2ms id=7f3a99c1e0b2
# The gateway sets X-Request-ID once; every service propagates it →
# one grep traces a user's request across the whole system.The Stack, the Built-ins, and Middleware vs Dependency
Ordering is an onion: last added, first entered. The built-ins solve solved problems — use them. Then the decision table that keeps concerns in the right layer.
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
app = FastAPI()
app.add_middleware(GZipMiddleware, minimum_size=1000) # compress bodies ≥ 1KB
app.add_middleware(TrustedHostMiddleware, # reject forged Host headers
allowed_hosts=["api.aicancode.org", "localhost"])
# also available: HTTPSRedirectMiddleware (usually the proxy's job instead)
# Onion order — add_middleware PREPENDS:
# app.add_middleware(A); app.add_middleware(B)
# request → B → A → routes → A → B → response
# Practical order: CORS outermost, then observability, then gzip.
# ── middleware vs dependency ──
# middleware dependency
# runs for every route routes that declare it
# knows endpoint/user no yes (typed, composable)
# can be overridden no yes (dependency_overrides)
# return early? yes (short-circuit) yes (raise HTTPException)
#
# request IDs, gzip, access logs → middleware
# auth, rate limits, tenant resolution → dependencies
# "auth in middleware" is the classic mistake: it can't vary per route,
# can't be overridden in tests, and turns /health into a 401.
# Performance note: each @app.middleware("http") wraps the app in
# BaseHTTPMiddleware — fine for a few; for hot-path work (or streaming
# quirks) drop to a pure ASGI middleware. Don't stack fifteen of them.Key Points to Remember
- 1@app.middleware("http"): code before await call_next is inbound, after is outbound
- 2request.state carries per-request context (request_id) to endpoints and handlers
- 3Onion stacking: added last runs first inbound; CORS goes outermost
- 4All-routes plumbing → middleware; per-route/testable concerns (auth) → dependencies
Interview Questions
Sign in to ask AriaImplement request tracing across three FastAPI microservices — where is the ID minted and how does it travel?
Why is authentication in middleware usually the wrong call in FastAPI?
You add middlewares A then B — draw the execution order for one request.
Ask Aria about Middleware — Code Around Every Request
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.