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_id if routes need the ID

Log in middleware:

python
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 response

Built-In Starlette Middleware

python
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):

text
Frontend http://localhost:8000  →  API http://localhost:8000

Cross-origin (CORS required):

text
Frontend http://localhost:5173  →  API http://localhost:8000

Concepts match Flask CORS chapter. SPA-side setup (Vite proxy, fetch, env vars): Vue Fetching API and CORS.

CORSMiddleware

Load origins from settings:

python
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=True required for cookies—cannot use allow_origins=["*"] with credentials
  • Preflight OPTIONS requests handled automatically

Warning

Tighten Origins in Production

Never use allow_origins=["*"] with authenticated cookies or sensitive APIs.

Dev Proxy Alternative

Vite vite.config.js:

javascript
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)

python
@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 response

Full checklist in security chapter.

Middleware Order

text
Request  →  CORS  →  GZip  →  custom logging  →  route
Response ←  CORS  ←  GZip  ←  custom logging  ←  route

Register 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.