🎓 What You Will Learn
- CORS Basics: Why browsers block cross-origin requests
- CORS Headers: Understanding request and response headers
- Preflight Requests: How browsers validate CORS before sending data
- Configuration: Setting up CORS middleware in FastAPI
- Credentials: Sending cookies and auth headers across origins
- Best Practices: Security and testing CORS implementations
1The CORS Problem: Why Browsers Block Requests
By default, browsers block JavaScript requests to a different domain (origin). This is a security feature. It stops bad websites from reading data from other sites. CORS is the rule set that lets your API say which origins are allowed.
Origin: A combination of protocol (http/https), domain, and port. http://localhost:3000 and http://localhost:3001 are different origins.
❌ CORS Blocked
Frontend on domain A tries to access API on domain B without proper CORS headers
Error: CORS policy blocks request
2Understanding CORS Headers
CORS works with special response headers. Your backend sends them. They tell the browser which origins may use the API.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://example.com"], # Allowed origins
allow_credentials=True, # Allow cookies
allow_methods=["*"], # All HTTP methods
allow_headers=["*"], # All headers
)
@app.get("/data")
async def get_data():
return {"message": "This is accessible from allowed origins"}
| CORS Header | Purpose | Example |
|---|---|---|
| Access-Control-Allow-Origin | Specifies which origins can access the API | https://example.com |
| Access-Control-Allow-Methods | HTTP methods allowed (GET, POST, etc) | GET, POST, PUT, DELETE |
| Access-Control-Allow-Headers | Request headers the client can send | Content-Type, Authorization |
| Access-Control-Allow-Credentials | Whether cookies/auth can be sent | true or false |
| Access-Control-Max-Age | How long preflight results are cached | 3600 (1 hour) |
3Setting Up CORS in FastAPI
FastAPI makes CORS simple with the built-in CORSMiddleware. Configure it during app initialization.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Basic CORS setup - allow all origins
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allow all origins (development only)
allow_methods=["*"], # Allow all methods
allow_headers=["*"], # Allow all headers
)
@app.get("/")
async def root():
return {"message": "CORS is now enabled"}
Security Warning: allow_origins=["*"] is dangerous in production. Any website could call your API. Always list your real domains.
4Allowing Specific Origins
In production, always specify which domains can access your API. This prevents unauthorized sites from making requests.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Specific origins for production
allowed_origins = [
"https://example.com",
"https://www.example.com",
"https://app.example.com",
"https://admin.example.com"
]
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
max_age=3600, # Cache preflight for 1 hour
)
@app.get("/api/data")
async def get_data():
return {"data": "sensitive information"}
5Preflight Requests: The OPTIONS Method
For some requests, the browser first sends an OPTIONS request. This is called a preflight. It asks the server: is the real request allowed?
Simple vs Preflighted Requests: Simple requests skip preflight. These are GET, HEAD, and POST with form content types (like form-urlencoded or multipart/form-data). A JSON body or a custom header always triggers a preflight first.
// The browser runs this JavaScript
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
},
body: JSON.stringify({name: 'John'})
})
// Because of the JSON body and custom header,
// the browser automatically sends:
// 1. OPTIONS request (preflight)
// - Server must respond with CORS headers
// - Server must allow the method and headers
// 2. Actual POST request (if preflight succeeds)
6Sending Credentials: Cookies & Auth Headers
By default, browsers don't include cookies or auth headers in cross-origin requests. Enable this with allow_credentials=True.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://frontend.example.com"],
allow_credentials=True, # Enable cookies and auth
allow_methods=["GET", "POST"],
allow_headers=["Content-Type", "Authorization"],
)
@app.get("/protected")
async def protected_endpoint():
# Cookies arrive here because allow_credentials=True
return {"message": "User authenticated via cookie"}
@app.post("/login")
async def login():
response = JSONResponse({"status": "logged in"})
# Cross-site cookies need samesite="none" and secure=True
response.set_cookie(
"session_id",
"abc123",
httponly=True,
secure=True,
samesite="none",
)
return response
Frontend side: Your fetch call must set credentials: "include". Without it, the browser will not send cookies to a different origin.
Important: Do not use allow_origins=["*"] together with allow_credentials=True. Browsers block the wildcard when credentials are on. List exact origins instead.
7Dynamic Origin Validation
For more control, build the origin list at startup. Read it from the environment, a config file, or a database.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import os
app = FastAPI()
def get_allowed_origins():
# Get from environment, database, or config file
if os.getenv("ENV") == "development":
return ["http://localhost:3000", "http://localhost:8000"]
else:
return [
"https://example.com",
"https://www.example.com",
"https://api.example.com"
]
allowed_origins = get_allowed_origins()
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def root():
return {"allowed_origins": allowed_origins}
8Testing CORS Implementation
Test CORS by simulating cross-origin requests from different domains.
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_cors_headers_present():
"""CORS headers appear for an allowed origin"""
response = client.get(
"/api/data",
headers={"Origin": "https://example.com"}
)
assert response.status_code == 200
assert "access-control-allow-origin" in response.headers
def test_cors_specific_origin():
"""The allowed origin is echoed back"""
response = client.get(
"/api/data",
headers={"Origin": "https://example.com"}
)
assert response.headers["access-control-allow-origin"] == "https://example.com"
def test_cors_disallowed_origin():
"""A preflight from an unknown origin is rejected"""
response = client.options(
"/api/data",
headers={
"Origin": "https://malicious.com",
"Access-Control-Request-Method": "POST",
},
)
# Starlette answers 400 for a disallowed preflight
assert response.status_code == 400
def test_preflight_request():
"""A valid preflight request succeeds"""
response = client.options(
"/api/data",
headers={
"Origin": "https://example.com",
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "Content-Type",
},
)
assert response.status_code == 200
assert "access-control-allow-methods" in response.headers
9Common CORS Issues & Solutions
| Issue | Cause | Solution |
|---|---|---|
| No Access-Control header | CORS middleware not configured | Add CORSMiddleware to app |
| Wrong origin in header | Origin header doesn't match allowed list | Add origin to allow_origins list |
| Preflight fails | Request method or headers not allowed | Add method/header to allow_methods or allow_headers |
| Credentials not sent | allow_credentials=False | Set allow_credentials=True |
| Wildcard with credentials | allow_origins=['*'] and allow_credentials=True | Use specific origins with credentials |
10Production CORS Configuration
Production CORS should be restrictive and environment-aware.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import os
app = FastAPI()
# Development vs Production
if os.getenv("ENV") == "development":
allowed_origins = [
"http://localhost:3000",
"http://localhost:3001",
"http://127.0.0.1:3000"
]
else:
allowed_origins = [
"https://example.com",
"https://www.example.com"
]
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
max_age=86400, # Cache for 24 hours
expose_headers=["X-Total-Count"] # Allow frontend to read custom headers
)
@app.get("/api/items")
async def list_items():
return {"items": []}
expose_headers: By default, JavaScript can only read standard headers. Use expose_headers to allow frontend to read custom headers like X-Total-Count for pagination.
11Debugging CORS Issues
When CORS blocks a request, the browser console shows the error. Use these techniques to debug.
# 1. Check preflight response
curl -X OPTIONS http://localhost:8000/api/data \
-H "Origin: https://example.com" \
-H "Access-Control-Request-Method: POST" \
-v
# 2. Check actual request
curl -X GET http://localhost:8000/api/data \
-H "Origin: https://example.com" \
-v
# 3. Look for headers in response:
# - Access-Control-Allow-Origin
# - Access-Control-Allow-Methods
# - Access-Control-Allow-Headers
12CORS Alternatives & Patterns
In some cases, CORS may not be the right solution. Consider these alternatives.
| Scenario | Alternative | When to Use |
|---|---|---|
| Internal microservices | API Gateway with mTLS | Service-to-service communication |
| Mobile apps | Token authentication | CORS is a browser rule — native apps skip it |
| Public API | OAuth2 + CORS | Third-party applications |
| Same domain | No CORS needed | Frontend and API on same domain |
| Limited access | Whitelist specific IPs | Server-to-server API calls |
13CORS Security Best Practices
- Always specify exact origins in production (never use wildcard)
- Use https:// in all production origins
- Only allow methods your API actually needs
- Restrict headers to what frontend actually sends
- Set max_age appropriately (1-24 hours)
- Use CORS with authentication (tokens, sessions)
- Test CORS with real browser clients
- Log CORS errors for security monitoring
14Advanced CORS Examples
Here is a full example. It logs every cross-origin request and exposes a custom pagination header.
from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
expose_headers=["X-Total-Count"], # Let the frontend read this header
)
# Log the Origin header of every request
@app.middleware("http")
async def log_origin(request: Request, call_next):
origin = request.headers.get("origin")
if origin:
print(f"CORS request from: {origin}")
return await call_next(request)
# Expose a custom header for pagination
@app.get("/api/items")
async def list_items(response: Response, skip: int = 0, limit: int = 10):
response.headers["X-Total-Count"] = "100"
return {"items": []}
15Summary & Key Takeaways
CORS is essential for modern web applications. Master it to build secure APIs that work seamlessly with frontend applications on different domains.
🚀 Congratulations! You now understand CORS, preflight requests, and how to securely configure your FastAPI API for cross-origin access. Build with confidence!