Chapter 12 · Middleware and CORS
Chapter goals: Understand middleware's position in the request/response lifecycle, learn to write custom middleware, master the execution order rules, and thoroughly understand
CORSMiddlewareparameters and common pitfalls.
12.1 What Is Middleware
Middleware is a function that fires on every request: it sees the request before any path operation, and sees the response after all path operations complete. Typical uses: logging, timing, auth pre-check, compression, CORS.
Workflow:
- Receive the request;
- Optionally preprocess the request;
- Pass the request downstream via
call_next(request)(routing → dependency → path operation); - Optionally postprocess the response;
- Return the response.
Execution timing detail
The官方 docs explicitly state: for dependencies with yield, their teardown code (the part after yield) runs AFTER middleware; background tasks also run after all middleware. So do not expect to observe "dependency cleanup complete" or "background task done" states inside middleware.
12.2 Custom HTTP Middleware: Request Timing
Defined with the @app.middleware("http") decorator. Classic example — add a processing-time header to responses:
# main.py
import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
# Before call_next = request phase (before entering the path operation)
start = time.perf_counter()
response = await call_next(request)
# After call_next = response phase
process_time = time.perf_counter() - start
# Official recommendation: use perf_counter() for higher precision than time.time()
response.headers["X-Process-Time"] = str(process_time)
return response
@app.get("/items/")
async def list_items():
return [{"name": "Foo"}]Two points to note:
call_nextis a coroutine; must beawaited;- Custom response headers must have an
X-prefix to be readable by browser-side JS, and must also be declared in CORS'sexpose_headers(see 12.5 below).
12.3 Execution Order of Multiple Middlewares
This is a high-frequency interview and debugging topic. Each added middleware is like wrapping an additional layer of onion: the last added is the outermost layer.
from fastapi import FastAPI, Request
app = FastAPI()
def log_middleware(name: str):
async def wrap(request: Request, call_next):
print(f"[{name}] request ->")
resp = await call_next(request)
print(f"[{name}] response <-")
return resp
return wrap
app.add_middleware(log_middleware("A"))
app.add_middleware(log_middleware("B"))Execution order:
Request direction: B → A → route handling
Response direction: route handling → A → BThat is, when add_middleware(MiddlewareB) is called after add_middleware(MiddlewareA), B is the outermost layer, and requests hit B first. This onion model guarantees predictable order — put the middleware that needs to intercept earliest (e.g., HTTPS redirect) on the outermost layer.
12.4 Native ASGI Middleware and Built-in Middleware
The decorator approach is essentially a wrapper around Starlette's BaseHTTPMiddleware — convenient but with overhead, and cannot finely control the raw scope. The lower-level approach is to implement an ASGI class middleware, registered via app.add_middleware():
from collections.abc import Awaitable, Callable
from typing import Any
from fastapi import FastAPI
app = FastAPI()
class CustomMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
# Request phase
# ... custom logic ...
await self.app(scope, receive, send)
# Response phase
# ... custom logic ...
app.add_middleware(CustomMiddleware)12.5 CORS: Cross-Origin Resource Sharing
Browsers enforce the same-origin policy: a page on example.com cannot AJAX-call api.example.com unless the latter explicitly allows it. CORS is the mechanism by which the server declares which origins are permitted.
FastAPI provides CORSMiddleware:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://frontend.example.com"], # Specific origin
# allow_origins=["*"], # ⚠️ Allows all — insecure for authenticated APIs
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
expose_headers=["X-Process-Time", "X-Request-ID"],
max_age=600, # Preflight cache duration in seconds
)Key parameters:
| Parameter | Meaning |
|---|---|
allow_origins | List of permitted origins; ["*"] allows all (insecure with credentials) |
allow_credentials | Whether to send Cookies with cross-origin requests |
allow_methods | HTTP methods allowed; ["*"] allows all |
allow_headers | Headers allowed in the request; ["*"] allows all |
expose_headers | Response headers the browser's JS is allowed to read |
max_age | How long the browser caches the preflight (OPTIONS) response |
Common CORS pitfall
allow_origins=["*"] + allow_credentials=True is invalid — browsers reject this combination. When credentials are needed, specify exact origins.
12.6 Chapter Summary
- Middleware wraps every request/response;
call_next(request)passes downstream; - Last-added middleware is outermost — execution order is reverse of registration order;
@app.middleware("http")decorator for simple middleware; ASGI class for full control;CORSMiddlewarewith exact origins (never*with credentials);expose_headersfor custom headers.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. What is the execution order of two middlewares A and B added in that order?
2. Why can you not use allow_origins=["*"] with allow_credentials=True?
3. What does expose_headers do in CORSMiddleware?
4. When does the code after call_next in a middleware run?
🛠️ Hands-On Exercises
- Write a middleware that logs
[REQUEST] path method timeand[RESPONSE] status_code process_timefor every request; verify output in the console. - Add CORSMiddleware allowing
https://frontend.example.comwith credentials; test with curl-H "Origin: https://frontend.example.com"and inspect theAccess-Control-Allow-Originresponse header. - Deliberately set
allow_origins=["*"]withallow_credentials=True; observe the browser error and fix by listing the exact origin.
Proceed to Chapter 13: Database Integration.