Error handling, middleware and CORS
Return consistent error shapes, log unhandled exceptions, time requests with middleware, and configure CORS so browsers can actually call your API.
Errors with a predictable shape
from fastapi import FastAPI, HTTPException, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
app = FastAPI()
class AppError(Exception):
def __init__(self, code: str, message: str, status: int = 400):
self.code, self.message, self.status = code, message, status
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
return JSONResponse(status_code=exc.status,
content={"error": {"code": exc.code, "message": exc.message}})
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
return JSONResponse(status_code=422,
content={"error": {"code": "validation_failed",
"fields": exc.errors()}})Handlers registered for specific exception classes replace the default response entirely. Raising HTTPException stays the right choice for ordinary cases: 401, 403, 404, 409.
Middleware
import time, uuid
from starlette.middleware.base import BaseHTTPMiddleware
class RequestContext(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
request_id = request.headers.get("x-request-id", str(uuid.uuid4()))
start = time.perf_counter()
try:
response = await call_next(request)
except Exception:
logger.exception("unhandled error request_id=%s", request_id)
raise
response.headers["x-request-id"] = request_id
response.headers["x-response-time"] = f"{(time.perf_counter() - start) * 1000:.1f}ms"
return response
app.add_middleware(RequestContext)- Middleware wraps every request, including requests to routes that do not exist.
- Ordering is the reverse of registration: the last middleware added is the outermost, so add CORS last if it must also cover error responses.
- An exception raised before
await call_nextnever reaches your route handlers, and an unhandled one bypassesexception_handlerregistrations made forException. - Keep middleware cheap: it runs for static files, health checks and every 404 as well.
CORS for browser clients
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)⚠️
Browsers reject
Access-Control-Allow-Origin: * together with credentials, and the error message says nothing about the cause. List origins explicitly when you send cookies; use a wildcard only for a genuinely public, credential-free API.FAQ
Why does Postman work but the browser fail?
CORS is enforced by the browser only. If the browser console shows a preflight failure, the
OPTIONS request is being blocked or redirected. Confirm the middleware is registered and that your reverse proxy forwards OPTIONS with the Origin header.Should I return 500 details to the client?
No. Log the traceback with the request id, and return a generic message. Leaking stack traces exposes file paths, library versions and sometimes credentials from connection strings.
Related
Your first FastAPI app Authentication with OAuth2, JWT and password hashing
Last refreshed 2026-09-18.