Middleware and CORS
Introduction
Middleware wraps every request and response—logging, timing, security headers. CORS lets browsers call your API from a different origin (e.g. Vite on port 5173, API on 8000). This chapter adds custom HTTP middleware and configures CORSMiddleware for frontend development and production.
Prerequisites
HTTP Middleware
Code explanation:
- Middleware must
await call_next(request)to invoke routes - Modify response before returning it
- Use
request.state.request_id = request_idif routes need the ID
Log in middleware:
import logging
logger = logging.getLogger("app")
@app.middleware("http")
async def log_requests(request: Request, call_next):
logger.info("Started %s %s", request.method, request.url.path)
response = await call_next(request)
logger.info("Completed %s %s -> %s", request.method, request.url.path, response.status_code)
return responseBuilt-In Starlette Middleware
from starlette.middleware.gzip import GZipMiddleware
app.add_middleware(GZipMiddleware, minimum_size=1000)Add with app.add_middleware()—order matters: last added runs first on the way in.
CORS Basics
Browsers block JavaScript from reading cross-origin responses unless the server sends Access-Control-Allow-* headers.
Same-origin example (no CORS needed):
Frontend http://localhost:8000 → API http://localhost:8000Cross-origin (CORS required):
Frontend http://localhost:5173 → API http://localhost:8000Concepts match Flask CORS chapter. SPA-side setup (Vite proxy, fetch, env vars): Vue Fetching API and CORS.
CORSMiddleware
Load origins from settings:
from app.core.config import get_settings
settings = get_settings()
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type"],
)Code explanation:
allow_credentials=Truerequired for cookies—cannot useallow_origins=["*"]with credentials- Preflight
OPTIONSrequests handled automatically
Warning
Tighten Origins in Production
Never use allow_origins=["*"] with authenticated cookies or sensitive APIs.
Dev Proxy Alternative
Vite vite.config.js:
export default {
server: {
proxy: {
"/api": "http://127.0.0.1:8000",
},
},
};Frontend calls /api/v1/... on same origin—no CORS in dev. Still configure CORS for staging/production.
Security Headers Middleware (Optional)
@app.middleware("http")
async def security_headers(request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
return responseFull checklist in security chapter.
Middleware Order
Request → CORS → GZip → custom logging → route
Response ← CORS ← GZip ← custom logging ← routeRegister CORS early with add_middleware so it wraps other layers correctly.
FAQ
CORS error in browser but curl works?
CORS is browser-only—fix CORSMiddleware origins and methods.
Credentials not sent?
Frontend fetch(..., { credentials: "include" }) plus matching allow_credentials.
Duplicate CORS headers?
Nginx and FastAPI both adding headers—disable one layer.
Middleware sync vs async?
Use async def middleware with FastAPI; call await call_next.
WebSocket CORS?
Separate from HTTP CORS—configure origins on WebSocket routes if needed.
Block middleware for /health?
Check request.url.path and skip logging for probes.