🎓 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 from making requests to different domains (origins). This is a security feature to prevent malicious websites from accessing sensitive data from other sites.
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 by having the backend include special headers in its response, telling the browser which origins are allowed to access the API.
from fastapi import FastAPI
from starlette.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 starlette.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=[*] in production is dangerous. Restrict to specific 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 starlette.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 certain requests, browsers automatically send an OPTIONS request first (called a preflight) to check if the actual request is allowed. This protects against unintended side effects.
Simple vs Complex Requests: Simple requests (GET, POST with content-type: form-data) skip preflight. Complex requests (POST with JSON, custom headers) require preflight.
# Client-side (browser)
# User submits a form via JavaScript
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
},
body: JSON.stringify({name: 'John'})
})
# 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, Depends
from starlette.middleware.cors import CORSMiddleware
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():
# This endpoint receives cookies from the request
# because allow_credentials=True
return {"message": "User authenticated via cookie"}
@app.post("/login")
async def login():
# Login endpoint sets a session cookie
from fastapi.responses import JSONResponse
response = JSONResponse({"status": "logged in"})
response.set_cookie("session_id", "abc123")
return response
Important: When using allow_credentials=True, you CANNOT use allow_origins=[*]. You must specify exact origins.
7Dynamic Origin Validation
For more control, validate origins dynamically based on environment or database.
from fastapi import FastAPI, Request
from starlette.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.
import pytest
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_cors_headers_present():
"""Test that CORS headers are in response"""
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():
"""Test that specific origins are allowed"""
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():
"""Test that disallowed origins are blocked"""
response = client.options(
"/api/data",
headers={"Origin": "https://malicious.com"}
)
# Browser would block this request
assert "access-control-allow-origin" not in response.headers or response.headers.get("access-control-allow-origin") != "https://malicious.com"
def test_preflight_request():
"""Test OPTIONS preflight request"""
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 response.headers["access-control-allow-methods"] is not None
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 starlette.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 | API Keys in headers | Native apps cannot be spoofed |
| 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
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
app = FastAPI()
# Custom CORS handling with logging
class LoggingCORSMiddleware(CORSMiddleware):
async def __call__(self, request: Request, call_next):
origin = request.headers.get("origin")
if origin:
print(f"CORS Request from: {origin}")
return await super().__call__(request, call_next)
app.add_middleware(
CORSMiddleware,
allow_origins=["https://example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
)
# Expose custom headers for pagination
@app.get("/api/items")
async def list_items(skip: int = 0, limit: int = 10):
total = 100
return {
"items": [],
"total": total, # Frontend reads via X-Total-Count header
}
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!