Search Topics
Search across all FastAPI topics
Middleware
Core ConceptMiddleware runs on every request before it reaches your endpoint, and on every response before it's sent back. Perfect for logging, timing, and authentication.
You add logging middleware to track request timing. It logs the request... but the response never comes back. Requests seem to vanish into thin air. Turns out you forgot await call_next(request).
# Your middleware
@app.middleware("http")
async def log_requests(request: Request, call_next):
start = time.time()
print(f"Request: {request.method} {request.url}")
# Oops... forgot to call call_next(request)
# The request stops HERE. The endpoint never runs.
# The client waits... and waits... and eventually times out.
# Client sees:
# curl: (52) Empty reply from serverQuestion
Why did the request disappear? Because middleware sits between the client and your endpoint. If your middleware doesn't explicitly pass the request forward, it never reaches your route handler. The request just... stops.
The Russian Doll Model
Think of middleware as layers wrapping your endpoint — like Russian dolls. The request passes through each layer going in, hits your endpoint, and then the response travels back out through each layer in reverse.
Client
Sends request
Middleware A
First in, last out
Middleware B
Second in, second out
Middleware C
Last in, first out
Endpoint
Your route handler
The key insight: middleware added last runs first (closest to the endpoint). This LIFO order matters when middleware depends on each other.
The Mental Model
What you just learned
Middleware wraps your endpoint — request flows in, response flows out
call_next(request) is the bridge — skip it and the request stops dead
Middleware runs in reverse registration order (LIFO): last added = closest to endpoint
Creating Middleware
Here's the pattern you'll use 90% of the time. Your middleware does something before the request, calls call_next, then does something with the response.
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
# BEFORE: runs before your endpoint
start_time = time.perf_counter()
response = await call_next(request) # <-- The magic line
# AFTER: runs after your endpoint returns
process_time = time.perf_counter() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response # Don't forget this either!Insight
Everything before await call_next(request) is your "request phase". Everything after is your "response phase". This single function handles both directions of the request-response cycle.
Middleware Flow Visualized
Watch how a request flows through each middleware layer, hits the endpoint, then the response travels back in reverse order.
Creating Middleware
What you just learned
Use @app.middleware('http') to create request/response middleware
call_next(request) passes the request to the next layer (or endpoint)
Code before call_next is the request phase, code after is the response phase
Built-in CORS Middleware
FastAPI includes built-in CORS middleware so you don't have to write it yourself. You'll use this any time your frontend and backend are on different domains.
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # Your frontend
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)Class-Based Middleware
For more complex middleware — where you need configuration or shared state — use the BaseHTTPMiddleware class. Same concept, more structure.
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request
from fastapi.responses import JSONResponse
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Check for auth header on every request
if not request.headers.get("Authorization"):
# Short-circuit: return early without calling call_next
return JSONResponse(
status_code=401,
content={"detail": "Not authenticated"},
)
# Auth header present — let the request through
response = await call_next(request)
return response
app.add_middleware(AuthMiddleware)The Missing call_next
You write middleware to reject requests without an API key. It works for blocked requests, but allowed requests hang forever.
@app.middleware("http")
async def check_api_key(request: Request, call_next):
api_key = request.headers.get("X-API-Key")
if not api_key:
return JSONResponse(
status_code=403,
content={"error": "API key required"}
)
# Great, API key exists! But... where's call_next?
# The request with a valid key just hangs here.
# Nothing happens. No response. Timeout.Peel the Layers
Middleware wraps your endpoint like Russian dolls. Peel away each layer to see what it does.
Go Deeper: Exception Handling in Middleware
What happens if your endpoint raises an exception? If you don't wrap call_next in a try/except, the exception bubbles up and your "response phase" code never runs.
@app.middleware("http")
async def error_handling_middleware(request: Request, call_next):
try:
response = await call_next(request)
except Exception as exc:
# Catch any unhandled exception from the endpoint
# Log it, return a clean error response
print(f"Unhandled error: {exc}")
return JSONResponse(
status_code=500,
content={"detail": "Internal server error"},
)
return responseWatch out
Be careful with error-handling middleware. If you catch too broadly, you might swallow exceptions that should crash the app (like SystemExit). Only catch Exception, not BaseException.
Advanced Middleware
What you just learned
Class-based middleware with BaseHTTPMiddleware gives you more structure
Middleware can short-circuit — return early without calling call_next
Wrap call_next in try/except to handle endpoint exceptions gracefully
Use middleware for global concerns, dependencies for per-route logic
Think about it...
If you have 3 middleware layers (A, B, C added in that order) and B raises an exception, does C's response code run?
Hint: Think about the order: A wraps B wraps C. Which layer is 'inside' which?
Key Points
Request → Response
Middleware wraps the entire request-response cycle
Execution Order
Middleware runs in reverse order of how it was added
Built-in CORS
CORSMiddleware handles cross-origin requests out of the box
vs Dependencies
Use middleware for global concerns, Depends for per-route logic