Middleware
Code that runs around every request. FastAPI inherits Starlette’s model, and the interview question is almost always about ordering or about the one middleware base class that quietly breaks streaming.
The two kinds, and why it matters
@app.middleware("http")
async def add_timing(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
response.headers["X-Time"] = f"{time.perf_counter() - start:.3f}"
return responseThat decorator wraps BaseHTTPMiddleware, which is convenient and has a real
cost: it buffers the response body. A streaming endpoint stops streaming,
and a large download is held in memory.
Pure ASGI middleware has no such problem, because it passes messages through rather than materialising a response:
class Timing:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
return await self.app(scope, receive, send)
start = time.perf_counter()
async def wrapped(message):
if message["type"] == "http.response.start":
took = f"{time.perf_counter() - start:.3f}"
message["headers"].append((b"x-time", took.encode()))
await send(message)
await self.app(scope, receive, wrapped)
app.add_middleware(Timing)Rule of thumb:
@app.middleware("http")for anything that only reads headers or sets one. Pure ASGI when the endpoint streams, when you touch the body, or when the middleware is on every request in a hot path.
The scope["type"] != "http" guard is not optional — without it the middleware
breaks WebSocket and lifespan messages, which is a confusing failure because
HTTP keeps working.
Order is the reverse of registration
app.add_middleware(GZipMiddleware) # added first
app.add_middleware(CORSMiddleware) # added second
app.add_middleware(TrustedHostMiddleware) # added thirdRequests pass through in reverse order of registration — TrustedHost, then CORS, then GZip — and responses come back the other way. So the last one added is the outermost.
That inversion is what breaks CORS on error responses. If an exception is
raised inside a middleware registered after CORS, the CORS headers never get
attached and the browser reports a CORS failure for what is actually a 500. Add
CORSMiddleware last so it is outermost, and the headers survive.
| Middleware | Register |
|---|---|
CORSMiddleware |
last, so it wraps everything |
TrustedHostMiddleware |
early, to reject bad Hosts cheaply |
GZipMiddleware |
early, so it compresses the final body |
| Your auth or logging | in between |
Middleware or dependency?
The distinction people get wrong. Both run before the handler; they differ in what they can see and what they can do.
| Middleware | Dependency | |
|---|---|---|
| Runs for | every request, including 404s | only routes that declare it |
| Knows the route | no | yes |
| Can return a value | no | yes, injected |
| Sees the response | yes | yes, after yield |
| Per-route control | manual path checks | APIRouter(dependencies=...) |
If you find yourself matching on request.url.path inside middleware, you
wanted a dependency. Auth belongs in a router-level dependency, not in
middleware doing prefix matching — see
Dependencies and injection.
Middleware is right for genuinely global concerns: request ids, timing, GZip, CORS, and catching what escapes everything else.
The request-id pattern
The one middleware nearly every service ends up with, and the reason is Correlation IDs and Trace Context in Async Python:
request_id: ContextVar[str] = ContextVar("request_id", default="")
@app.middleware("http")
async def correlate(request: Request, call_next):
rid = request.headers.get("X-Request-ID") or uuid4().hex
token = request_id.set(rid)
try:
response = await call_next(request)
response.headers["X-Request-ID"] = rid
return response
finally:
request_id.reset(token)A ContextVar rather than a global, because concurrent requests share the
process. It propagates across await and, importantly, does not propagate
into a raw thread — which is why a logging filter reading it from
run_in_executor gets an empty string.
Related
Interview angle 5
- “How does FastAPI middleware ordering work?” - requests pass through in reverse order of registration, so the last one added is outermost and responses unwind the other way. That inversion is why
CORSMiddlewaregoes last: registered earlier, it misses error responses and the browser reports a CORS failure for a 500. - “What’s wrong with
@app.middleware("http")?” - it wrapsBaseHTTPMiddleware, which buffers the response body. A streaming endpoint stops streaming and a large download sits in memory. Write pure ASGI middleware when the endpoint streams or the middleware is in a hot path. - “What must pure ASGI middleware do that people forget?” - guard on
scope["type"] != "http"and pass everything else through. Without it, WebSocket and lifespan messages break while ordinary HTTP keeps working, which makes it hard to spot. - “Middleware or dependency?” - middleware for genuinely global concerns: request ids, timing, GZip, CORS. A dependency when the logic is route-specific or needs to inject a value. Matching on
request.url.pathinside middleware means you wanted a dependency. - “How do you correlate logs across a request?” - a
ContextVarset in middleware from an inbound header or a fresh uuid, echoed on the response. It propagates acrossawaitbut not into a raw thread, which is why executor work loses it.