FastAPI: Fixing CORS Policy Errors with React and Frontend Apps
This error occurs when Python tries to serve cross-origin HTTP requests without configuring CORSMiddleware. Add CORSMiddleware to your FastAPI application with explicit allowed origins, methods, and headers.
Root Cause Analysis
This error occurs when Python backend services running FastAPI fail to provide the required HTTP access-control headers in response to cross-origin requests made by browser-based frontend applications.
Root Cause 1: Browser Same-Origin Policy (SOP)
Modern web browsers enforce the Same-Origin Policy for security. When a React application running on http://localhost:3000 attempts to make an XMLHttpRequest or fetch call to a FastAPI backend on http://localhost:8000, the origin (scheme, host, and port) does not match. The browser blocks JavaScript from reading the response unless the backend explicitly includes the Access-Control-Allow-Origin header in its HTTP response.
Root Cause 2: Missing Preflight OPTIONS Handling
For non-simple HTTP requests (such as requests sending Content-Type: application/json, custom Authorization headers, or PUT/DELETE/PATCH methods), browsers send an automatic preflight OPTIONS request before sending the actual request. If FastAPI is not configured with CORSMiddleware, it returns a 405 Method Not Allowed or responds without CORS headers, causing the browser to abort the primary request.
Root Cause 3: Middleware Registration Order
In Starlette and FastAPI, middlewares are executed in reverse order of addition for incoming requests and standard order for outgoing responses. If custom middlewares intercept or short-circuit the request cycle before CORSMiddleware has evaluated the preflight request, the CORS headers will not be attached to error responses or redirect responses.
Root Cause 4: The Wildcard Origin with Credentials Pitfall
According to the W3C CORS specification, when a frontend application sends requests with credentials (credentials: 'include' or cookies / HTTP authentication), the backend must NOT return Access-Control-Allow-Origin: *. Combining allow_origins=["*"] with allow_credentials=True is explicitly rejected by modern browsers and will cause CORS errors.
Reproduction Code (MCVE)
from fastapi import FastAPI
from starlette.testclient import TestClient
app = FastAPI()
@app.get("/api/data")
def read_data():
return {"status": "ok", "items": [1, 2, 3]}
# Test client simulating React app on http://localhost:3000 sending preflight OPTIONS
client = TestClient(app)
response = client.options(
"/api/data",
headers={
"Origin": "http://localhost:3000",
"Access-Control-Request-Method": "GET",
"Access-Control-Request-Headers": "authorization,content-type"
}
)
# In the absence of CORSMiddleware, FastAPI returns no Access-Control-Allow-Origin header
if "access-control-allow-origin" not in response.headers:
raise AssertionError(
"CORS policy error: Missing 'Access-Control-Allow-Origin' header in preflight response. "
"React app at http://localhost:3000 was blocked by browser Same-Origin Policy."
)
Solution 1: Add CORSMiddleware with Explicit Allowed Origins
Add CORSMiddleware to your FastAPI application, listing the explicit frontend domains, allowing credentials, and enabling all standard methods and headers.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.testclient import TestClient
app = FastAPI()
# Configure allowed origins (e.g. local React dev server and staging domain)
origins = [
"http://localhost:3000",
"http://127.0.0.1:3000",
"https://myapp.example.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"],
allow_headers=["*"],
)
@app.get("/api/data")
def get_data():
return {"status": "ok", "items": [1, 2, 3]}
# Test preflight OPTIONS request with configured CORS middleware
client = TestClient(app)
response = client.options(
"/api/data",
headers={
"Origin": "http://localhost:3000",
"Access-Control-Request-Method": "GET"
}
)
print("Preflight Status:", response.status_code)
print("Allow-Origin Header:", response.headers.get("access-control-allow-origin"))
assert response.headers.get("access-control-allow-origin") == "http://localhost:3000"
Solution 2: Dynamic Environment-Driven CORS Settings
Use environment variables to manage allowed CORS origins across development, staging, and production environments cleanly.
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Load origins from environment variable with fallback for local development
raw_origins = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:5173")
allowed_origins = [origin.strip() for origin in raw_origins.split(",") if origin.strip()]
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/health")
def health_check():
return {"status": "healthy", "cors_enabled_for": allowed_origins}
print("Configured CORS origins:", allowed_origins)
A widespread mistake is placing trailing slashes in origin URLs (e.g. "http://localhost:3000/" instead of "http://localhost:3000"). The HTTP Origin header sent by browsers never includes a trailing slash or path. Having a trailing slash in allow_origins will result in string comparison mismatch, causing silent CORS rejection.
Another common edge case occurs when custom authentication middleware returns a 401 Unauthorized or 403 Forbidden before CORSMiddleware processes the response. If your custom middleware does not pass through CORS headers on error responses, the browser will report a misleading CORS error instead of displaying the actual 401 status code.
Contrast CORS errors with HTTP 404 or 500 errors: A CORS error is strictly a browser-side security enforcement. The FastAPI server may have executed the endpoint logic successfully, but the browser discards the payload before Javascript can access it.