You’re building a FastAPI backend, and your frontend can’t connect. Open the browser console and there it is: “Access to fetch at ‘http://localhost:8000/api/users’ from origin ‘http://localhost:3000’ has been blocked by CORS policy.” Sound familiar? CORS errors are probably the most frustrating issue for developers building modern web applications with separate frontend and backend services.
TLDR: Quick Fix for CORS Errors
Most Common Cause: FastAPI doesn’t enable CORS by default - you need to explicitly add the middleware.
❌ BAD (causes CORS errors):
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/users")
def get_users():
return [{"name": "Alice"}, {"name": "Bob"}]
# No CORS middleware = browser blocks requests from different origins
✅ GOOD (fixes it):
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # Your frontend URL
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/users")
def get_users():
return [{"name": "Alice"}, {"name": "Bob"}]
Other Common Causes:
- Using wildcard origin (
*) with credentials - Wrong origin URL (protocol, port, or domain mismatch)
- Missing preflight OPTIONS response
- CORS middleware added after routes
- Incorrect allowed methods or headers
What is a CORS Error?
CORS stands for Cross-Origin Resource Sharing. It’s a security feature built into browsers that blocks web pages from making requests to a different domain than the one serving the page. When your React app running on http://localhost:3000 tries to call your FastAPI backend on http://localhost:8000, the browser sees these as different origins and blocks the request unless the backend explicitly allows it.
Here’s what a typical CORS error looks like in your browser console:
Access to fetch at 'http://localhost:8000/api/users' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
The browser won’t even let you see the response from your API. It blocks the request completely, which is why your network requests fail even though your backend might be working perfectly fine.
Why Does This Error Happen?
CORS is a browser security mechanism designed to prevent malicious websites from making unauthorized requests to your API. Imagine if any random website could make requests to your banking API using your logged-in session - that’d be a disaster. So browsers implement the “same-origin policy” by default.
An origin is defined by three things:
- Protocol (http vs https)
- Domain (localhost vs example.com)
- Port (3000 vs 8000)
If any of these differ, it’s a different origin. That’s why http://localhost:3000 and http://localhost:8000 are considered different origins, even though they’re both on your local machine.
Common CORS Error Scenarios
1. No CORS Middleware at All
This is the number one mistake. FastAPI doesn’t enable CORS by default - you have to add it yourself.
❌ BAD (no CORS configuration):
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/data")
def get_data():
return {"message": "Hello from FastAPI"}
# When frontend tries to call this, browser blocks it
# Error: No 'Access-Control-Allow-Origin' header is present
✅ GOOD (CORS middleware configured):
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Configure CORS before defining routes
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # Frontend origin
allow_credentials=True,
allow_methods=["*"], # Allows all methods (GET, POST, PUT, DELETE, etc.)
allow_headers=["*"], # Allows all headers
)
@app.get("/api/data")
def get_data():
return {"message": "Hello from FastAPI"}
# Now frontend can successfully call this endpoint
2. Wrong Origin URL
CORS is extremely strict about matching origins. If you specify the wrong URL - even a tiny difference like trailing slash or protocol - it won’t work.
❌ BAD (mismatched origins):
# Backend allows https, but frontend uses http
app.add_middleware(
CORSMiddleware,
allow_origins=["https://localhost:3000"], # Note: https
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Frontend request from http://localhost:3000 gets blocked
# Error: Origin http://localhost:3000 is not allowed
✅ GOOD (exact origin match):
# Match exactly what your frontend uses
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000", # Development
"https://myapp.com", # Production
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
3. Using Wildcard with Credentials
You can’t use allow_origins=["*"] (wildcard) together with allow_credentials=True. Browsers explicitly forbid this combination for security reasons.
❌ BAD (wildcard + credentials):
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Wildcard
allow_credentials=True, # Can't use both!
allow_methods=["*"],
allow_headers=["*"],
)
# Error: The value of the 'Access-Control-Allow-Origin' header in the response
# must not be the wildcard '*' when the request's credentials mode is 'include'.
✅ GOOD (specific origins with credentials):
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"https://myapp.com"
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
If you really don’t need credentials (cookies, auth headers), you can use wildcard:
✅ ACCEPTABLE (wildcard without credentials):
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allow all origins
allow_credentials=False, # No credentials
allow_methods=["*"],
allow_headers=["*"],
)
# This works, but you can't send cookies or auth tokens
4. CORS Middleware Added in Wrong Order
Middleware order matters in FastAPI. If you add CORS middleware after defining routes, it won’t work properly.
❌ BAD (middleware after routes):
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Routes defined first
@app.get("/api/users")
def get_users():
return [{"name": "Alice"}]
# CORS middleware added after routes - TOO LATE!
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Still get CORS errors because middleware doesn't wrap routes
✅ GOOD (middleware before routes):
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Add CORS middleware FIRST
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Then define routes
@app.get("/api/users")
def get_users():
return [{"name": "Alice"}]
5. Preflight Request Failures
For certain requests (like POST with custom headers), browsers send a “preflight” OPTIONS request first to check if the actual request is allowed. If this preflight fails, your actual request never gets sent.
❌ BAD (missing OPTIONS support):
# If you manually handle OPTIONS and forget to include CORS headers
@app.options("/api/users")
def options_users():
return {} # Missing CORS headers!
# Preflight fails, actual request blocked
✅ GOOD (let middleware handle it):
# Don't manually handle OPTIONS - let CORSMiddleware do it
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"], # This automatically handles OPTIONS
allow_headers=["*"],
)
@app.post("/api/users")
def create_user(user: dict):
return user
# CORSMiddleware automatically responds to OPTIONS preflight requests
6. Restricted Methods or Headers
Sometimes you’ll get CORS errors for specific HTTP methods or headers if you haven’t allowed them.
❌ BAD (restrictive configuration):
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["GET"], # Only GET allowed
allow_headers=["Content-Type"], # Only Content-Type allowed
)
# Frontend tries to make a POST request with Authorization header
# Error: Method POST is not allowed by Access-Control-Allow-Methods
# Error: Header Authorization is not allowed by Access-Control-Allow-Headers
✅ GOOD (permissive configuration):
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"], # Allow all HTTP methods
allow_headers=["*"], # Allow all headers
)
# Or be specific about what you need
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
allow_headers=["Content-Type", "Authorization", "X-Custom-Header"],
)
Debugging CORS Errors
1. Check Browser Console
CORS errors always show up in the browser console, not in your FastAPI logs. Press F12 and look at the Console tab. The error message tells you exactly what’s wrong:
Access to fetch at 'http://localhost:8000/api/users' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
Key phrases to look for:
- “No ‘Access-Control-Allow-Origin’ header” → CORS not configured
- “Origin … is not allowed” → Wrong origin in allow_origins
- “Method … is not allowed” → Need to add method to allow_methods
- “Request header … is not allowed” → Need to add header to allow_headers
2. Check Network Tab
Open your browser’s Network tab (F12 → Network) and look at the failed request. Check:
- Request Headers: Look at the
Originheader - this is what your frontend is sending - Response Headers: Look for
Access-Control-Allow-Origin- if it’s missing, CORS isn’t configured
For preflight requests, you’ll see an OPTIONS request. If this fails, your actual request never gets sent.
3. Test with curl
CORS is a browser security feature - it doesn’t affect curl or Postman. If your API works with curl but not from browser, it’s definitely a CORS issue:
# This works (no CORS check)
curl http://localhost:8000/api/users
# But browser request from http://localhost:3000 fails (CORS blocks it)
4. Enable Debug Logging
Add logging to see what’s happening:
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
app = FastAPI()
@app.middleware("http")
async def log_requests(request: Request, call_next):
logger.debug(f"Request origin: {request.headers.get('origin')}")
logger.debug(f"Request method: {request.method}")
response = await call_next(request)
logger.debug(f"Response headers: {response.headers}")
return response
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
5. Verify Origin Exactly
Print out what origins you’re allowing vs what’s being requested:
origins = ["http://localhost:3000"]
@app.middleware("http")
async def check_origin(request: Request, call_next):
client_origin = request.headers.get("origin")
print(f"Client origin: '{client_origin}'")
print(f"Allowed origins: {origins}")
print(f"Match: {client_origin in origins}")
return await call_next(request)
Production CORS Configuration
Don’t use allow_origins=["*"] in production. It’s a security risk and won’t work with credentials anyway. Here’s how to configure CORS properly for different environments:
Environment-Based Configuration
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import os
app = FastAPI()
# Get allowed origins from environment variable
ALLOWED_ORIGINS = os.getenv(
"ALLOWED_ORIGINS",
"http://localhost:3000,http://localhost:5173" # Default for dev
).split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Then set the environment variable in production:
# Production
export ALLOWED_ORIGINS="https://myapp.com,https://www.myapp.com"
# Development
export ALLOWED_ORIGINS="http://localhost:3000,http://localhost:5173"
Using Settings/Config
from pydantic_settings import BaseSettings
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
class Settings(BaseSettings):
allowed_origins: list[str] = ["http://localhost:3000"]
class Config:
env_file = ".env"
settings = Settings()
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=settings.allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
In your .env file:
ALLOWED_ORIGINS=["https://myapp.com", "https://www.myapp.com"]
Regex Pattern Matching
For more complex scenarios with multiple subdomains:
from fastapi.middleware.cors import CORSMiddleware
import re
app = FastAPI()
# Match any subdomain of myapp.com
origin_regex = re.compile(r"https://.*\.myapp\.com")
app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"https://.*\.myapp\.com", # Built-in regex support
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Common CORS Patterns
1. Development vs Production
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import os
app = FastAPI()
# Different origins for different environments
if os.getenv("ENVIRONMENT") == "production":
origins = [
"https://myapp.com",
"https://www.myapp.com",
]
else:
# More permissive in development
origins = [
"http://localhost:3000",
"http://localhost:5173",
"http://127.0.0.1:3000",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
2. Mobile App + Web Frontend
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000", # Web dev
"https://myapp.com", # Web prod
"capacitor://localhost", # Capacitor mobile app
"ionic://localhost", # Ionic mobile app
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
3. Public API (No Credentials)
# For public APIs that don't use authentication
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allow any origin
allow_credentials=False, # No cookies or auth
allow_methods=["GET"], # Read-only
allow_headers=["*"],
)
4. Specific Headers Only
# If you know exactly which headers you need
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=[
"Content-Type",
"Authorization",
"X-Request-ID",
],
)
Testing CORS Configuration
Manual Testing with JavaScript
// Test from browser console on http://localhost:3000
fetch('http://localhost:8000/api/users', {
method: 'GET',
credentials: 'include', // Send cookies
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
}
})
.then(response => response.json())
.then(data => console.log('Success:', data))
.catch(error => console.error('Error:', error));
Automated Testing
from fastapi.testclient import TestClient
client = TestClient(app)
def test_cors_get_request():
response = client.get(
"/api/users",
headers={"Origin": "http://localhost:3000"}
)
assert response.status_code == 200
assert "access-control-allow-origin" in response.headers
assert response.headers["access-control-allow-origin"] == "http://localhost:3000"
def test_cors_preflight():
response = client.options(
"/api/users",
headers={
"Origin": "http://localhost:3000",
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "Content-Type,Authorization"
}
)
assert response.status_code == 200
assert "access-control-allow-methods" in response.headers
Frequently Asked Questions
Why does my API work in Postman but not in the browser?
CORS is a browser security feature. Postman, curl, and other HTTP clients don’t enforce CORS - only browsers do. If it works in Postman but fails in browser, it’s definitely a CORS configuration issue.
Can I disable CORS completely?
You can’t disable CORS in the browser - it’s a built-in security feature. But you can configure your FastAPI backend to allow all origins with allow_origins=["*"], which effectively means any website can call your API. This is fine for public APIs but risky for authenticated endpoints.
Why can’t I use wildcard origins with credentials?
It’s a security restriction in the CORS specification. If browsers allowed Access-Control-Allow-Origin: * with credentials, any malicious website could make authenticated requests to your API using the user’s cookies or tokens. By requiring specific origins, you explicitly control which websites can make authenticated requests.
What’s a preflight request and why is it failing?
A preflight request is an OPTIONS request that browsers send before certain “non-simple” requests (like POST with custom headers). The browser checks if the server allows the actual request before sending it. If your OPTIONS handler doesn’t return proper CORS headers, the preflight fails and your actual request never gets sent. Let CORSMiddleware handle OPTIONS automatically.
Do I need CORS if my frontend and backend are on the same domain?
No. If your frontend is served from https://myapp.com and your API is at https://myapp.com/api, they’re the same origin (same protocol, domain, and port). You don’t need CORS middleware. CORS only applies when origins differ.
How do I allow multiple subdomains?
Use the allow_origin_regex parameter with a regex pattern like r"https://.*\.myapp\.com" to match any subdomain of myapp.com. This is more maintainable than listing every subdomain individually.
Summary
CORS errors in FastAPI happen when your browser blocks requests from your frontend to your backend because they’re on different origins. The most common causes are:
- Not adding CORSMiddleware at all
- Wrong origin URL in allow_origins
- Using wildcard with credentials
- Adding middleware after routes
Remember to:
- Add CORSMiddleware before defining routes
- Use exact origin URLs (including protocol and port)
- Use specific origins in production, not wildcards
- Let the middleware handle OPTIONS preflight requests
- Test CORS in a real browser, not just with curl
CORS configuration is straightforward once you understand the rules. The key is being explicit about which origins, methods, and headers you allow. Start restrictive in production and only open up what you actually need.
Related Posts:
Debugging Stack Traces? Use Debugly’s trace formatter to quickly parse and analyze Python tracebacks.