Skip to main content

CORS Configuration

CORS (Cross-Origin Resource Sharing) controls which browser origins can call your API. Without it, browsers block requests from a frontend at app.example.com to an API at api.example.com. FastAPI includes CORS middleware out of the box.

Learning Focus

By the end of this lesson you can: configure CORS with allowed origins, enable credentials, restrict methods and headers, and safely manage origin lists across environments.

Adding CORSMiddleware

app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000", "https://app.example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["Authorization", "Content-Type", "X-Request-ID"],
)

CORS Options Reference

OptionTypeDescription
allow_originslist[str]Exact allowed origins or ["*"]
allow_origin_regexstrRegex pattern for origins
allow_credentialsboolAllow cookies/auth headers
allow_methodslist[str]HTTP methods or ["*"]
allow_headerslist[str]Allowed request headers or ["*"]
expose_headerslist[str]Headers browsers can access in response
max_ageintPreflight cache duration (seconds)

Environment-Based Origin Lists

app/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import field_validator

class Settings(BaseSettings):
ALLOWED_ORIGINS: list[str] = ["http://localhost:3000"]

@field_validator("ALLOWED_ORIGINS", mode="before")
@classmethod
def parse_origins(cls, v):
if isinstance(v, str):
return [o.strip() for o in v.split(",")]
return v

model_config = SettingsConfigDict(env_file=".env")

settings = Settings()
.env
ALLOWED_ORIGINS=http://localhost:3000,https://staging.example.com,https://app.example.com
app/main.py
from app.core.config import settings

app.add_middleware(
CORSMiddleware,
allow_origins=settings.ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

Wildcard Origins with Credentials

warning

allow_origins=["*"] and allow_credentials=True cannot be used together. Browsers reject this combination. For credentials, always list specific origins.

app/main.py
# ❌ This combination will not work — browsers reject it
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True, # Conflict!
)

# ✅ Correct: specific origins with credentials
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_credentials=True,
)

Dynamic Origin Validation

For multi-tenant apps where origins are stored in a database:

app/middleware/cors_dynamic.py
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response

ALLOWED_ORIGINS: set[str] = set()

async def refresh_allowed_origins(db: AsyncSession) -> None:
"""Call at startup to load origins from DB."""
global ALLOWED_ORIGINS
from sqlalchemy import select
result = await db.execute(select(TenantOrigin.origin))
ALLOWED_ORIGINS = set(result.scalars().all())

class DynamicCORSMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next) -> Response:
origin = request.headers.get("origin", "")
response = await call_next(request)
if origin in ALLOWED_ORIGINS:
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Vary"] = "Origin"
return response

Common Pitfalls

PitfallCause / SymptomFix
CORS error in browser but not curlcurl doesn't send Origin headerTest with browser dev tools or use --header "Origin: http://..."
Preflight OPTIONS returns 405CORS middleware not configuredEnsure CORSMiddleware is added before routers
Credentials not sentForgot allow_credentials=True or frontend credentials: "include"Both server and client must opt in
* origin not working for authBrowser blocks Authorization header with *List specific origins when using auth headers
Origin list not updating after deployConfig read at startupRestart the app or use dynamic CORS middleware

Hands-On Practice

app/main.py
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# Read from environment; fall back to localhost for development
allowed_origins = os.getenv(
"ALLOWED_ORIGINS",
"http://localhost:3000,http://localhost:5173"
).split(",")

app.add_middleware(
CORSMiddleware,
allow_origins=[o.strip() for o in allowed_origins],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type", "X-Request-ID"],
expose_headers=["X-Request-ID", "X-Response-Time"],
max_age=600,
)

@app.get("/")
async def root() -> dict:
return {"status": "ok"}
test-cors.sh
uvicorn app.main:app --reload

# Simulate browser preflight
curl -v -X OPTIONS http://localhost:8000/ \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Authorization"

# Should see:
# Access-Control-Allow-Origin: http://localhost:3000
# Access-Control-Allow-Credentials: true

What's Next