FastAPI CORS 에러 해결 방법
A complete guide to fixing CORS (Cross-Origin Resource Sharing) errors in FastAPI — from simple misconfigurations to complex proxy setups.
FastAPI CORS 에러 해결 방법
Introduction
CORS errors are among the most common issues when building web APIs with FastAPI. They occur when your browser-based frontend tries to call your FastAPI backend from a different origin, and the browser's security policy blocks the request. The error messages in the browser console are often confusing and do not clearly indicate what configuration is wrong.
I spent an entire afternoon debugging CORS on a FastAPI project that served a React frontend. The API worked perfectly in Postman and curl, but the browser kept blocking requests. The root cause was a mismatch between the allowed origins in the CORS middleware and the actual URL the browser was sending requests from.
Environment
FastAPI 0.111.0
Python 3.12.3
Uvicorn 0.30.1
React 18.3.1 (frontend)Problem
When your frontend makes a cross-origin request, you see errors like:
Access to XMLHttpRequest at 'http://localhost:8000/api/data' from origin
'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin'
header is present on the requested resource.Or for preflight requests:
Access to XMLHttpRequest at 'http://localhost:8000/api/data' from origin
'http://localhost:3000' has been blocked by CORS policy: Response to preflight request
doesn't pass access control check: It does not have HTTP ok status.Or the infamous wildcard error:
Access to XMLHttpRequest at 'http://localhost:8000/api/data' from origin
'http://localhost:3000' has been blocked by CORS policy: 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'.Analysis
CORS errors occur because browsers enforce the Same-Origin Policy. When your frontend at http://localhost:3000 makes a request to your API at http://localhost:8000, this is a cross-origin request. The browser requires the server to explicitly allow this.
There are three types of CORS requests:
Simple requests: GET, POST, or HEAD requests with standard headers. The browser sends the request directly.
Preflight requests: PUT, DELETE, or requests with custom headers. The browser first sends an OPTIONS request to check if the server allows the actual request.
Credentialed requests: Requests that include cookies or authorization headers. These require explicit origin specification (no wildcards).
Common misconfigurations:
# WRONG: Missing CORS middleware entirely
from fastapi import FastAPI
app = FastAPI()
@app.get("/api/data")
async def get_data():
return {"data": "hello"}
# Browser blocks all cross-origin requests
# WRONG: Using * with credentials
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Can't use * with credentials
allow_credentials=True, # This combination fails
allow_methods=["*"],
allow_headers=["*"],
)
# WRONG: Origin with trailing slash
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000/"], # Trailing slash!
)Solution
Fix 1: Add CORS middleware with correct configuration.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"http://localhost:5173", # Vite dev server
"https://myapp.com", # Production
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type"],
expose_headers=["X-Request-ID"],
)
@app.get("/api/data")
async def get_data():
return {"data": "hello"}Fix 2: For development, allow all origins safely.
import os
if os.getenv("ENVIRONMENT") == "development":
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
else:
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.com"],
allow_credentials=True,
allow_methods=["GET", "POST"],
allow_headers=["Authorization"],
)Fix 3: Debug by checking the preflight response.
# Test the OPTIONS preflight request
curl -X OPTIONS http://localhost:8000/api/data \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-vExpected response headers:
< HTTP/1.1 200 OK
< access-control-allow-origin: http://localhost:3000
< access-control-allow-methods: GET,POST,PUT,DELETE,OPTIONS
< access-control-allow-headers: Authorization,Content-Type
< access-control-max-age: 600Fix 4: Handle middleware ordering correctly.
# CORS middleware must be added FIRST (it wraps all other middleware)
app.add_middleware(CORSMiddleware, ...) # Add this first
# Other middleware after
app.add_middleware(SomeOtherMiddleware) # This secondFix 5: For WebSocket connections.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# WebSocket CORS is handled differently
# Allow all origins for WebSocket in development
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# WebSocket connections don't use CORS headers
# The browser handles WebSocket security differently
@app.websocket("/ws")
async def websocket_endpoint(websocket):
await websocket.accept()
await websocket.send_json({"message": "connected"})Lessons Learned
- CORS middleware must be added before all other middleware in FastAPI for it to work correctly.
- Never use
allow_origins=["*"]withallow_credentials=Truein production — it violates the CORS specification. - Always test preflight requests separately using curl with the OPTIONS method.
- Check for trailing slashes in origin URLs —
http://localhost:3000andhttp://localhost:3000/are different origins. - Use environment variables to switch between permissive development CORS and strict production CORS.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.