Middleware and CORS

FastAPI · Practical · 12 min

Timing headers, request ids and CORS are cross-cutting concerns that belong in one place, not in every endpoint.

Middleware wraps the whole application: it sees every request before any route runs and every response before it leaves. The quickest form is a decorator:

@app.middleware("http")
async def add_timing(request: Request, call_next):
    start = time.perf_counter()
    response = await call_next(request)
    response.headers["X-Process-Time"] = f"{time.perf_counter() - start:.4f}"
    return response

call_next passes the request down the stack and returns the response. Middleware added later wraps the earlier ones, so the last one added runs first on the way in. Keep middleware for truly global concerns: request ids, timing, security headers, GZipMiddleware. Per-route logic belongs in dependencies, which see validated parameters and appear in the docs.

CORS (Cross-Origin Resource Sharing) is the middleware almost everyone needs. A page served from https://app.example.com calling https://api.example.com is cross-origin, and the browser won't let the page read the response unless the API answers with matching Access-Control-Allow-* headers. For requests with JSON bodies, custom headers or methods like PUT and DELETE, the browser first sends an OPTIONS preflight. CORSMiddleware handles all of it:

  • allow_origins: exact origins, scheme + host + port, no path or trailing slash.
  • allow_credentials=True if the browser sends cookies or Authorization headers. The CORS rules forbid a wildcard origin together with credentials, so list origins explicitly.
  • allow_methods and allow_headers allow very little by default; ["*"] is common.

CORS is a browser rule, not protection for your API: curl, scripts and servers ignore it entirely. Authentication still has to guard your endpoints.

Example

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.testclient import TestClient

app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com", "http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/data")
def data():
    return {"ok": True}

c = TestClient(app)
pre = c.options("/data", headers={"Origin": "http://localhost:5173",
                                  "Access-Control-Request-Method": "GET"})
print(pre.status_code, pre.headers["access-control-allow-origin"])
# 200 http://localhost:5173
other = c.get("/data", headers={"Origin": "https://evil.example"})
print("access-control-allow-origin" in other.headers)
# False: the browser won't let that page read the response

Remember

  • Middleware wraps every request; the last added runs first on the way in
  • Use middleware for global concerns, dependencies for per-route logic
  • CORSMiddleware answers preflights and adds Access-Control-Allow-* headers
  • CORS only constrains browsers; it is not authentication

Watch out for

  • Adding a trailing slash or path to an origin (https://app.example.com/), which never matches.

Check yourself

  1. Your React app on http://localhost:5173 gets a CORS error calling your API, but the same request works with curl. Why?
  2. You call app.add_middleware(A) and then app.add_middleware(B). Which sees the request first?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: fastapi.tiangolo.com/tutorial/cors/