FastAPI

FastAPI CORS — Cross-Origin Resource Sharing Configuration

Thirdy Gayares
13 min read

🎓 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
CORSAPISecurityMiddleware

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

Frontend: http://localhost:3000
Backend: http://api.example.com
BLOCKED

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.

app/main.py
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 HeaderPurposeExample
Access-Control-Allow-OriginSpecifies which origins can access the APIhttps://example.com
Access-Control-Allow-MethodsHTTP methods allowed (GET, POST, etc)GET, POST, PUT, DELETE
Access-Control-Allow-HeadersRequest headers the client can sendContent-Type, Authorization
Access-Control-Allow-CredentialsWhether cookies/auth can be senttrue or false
Access-Control-Max-AgeHow long preflight results are cached3600 (1 hour)

3Setting Up CORS in FastAPI

FastAPI makes CORS simple with the built-in CORSMiddleware. Configure it during app initialization.

app/main.py
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.

app/main.py
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.

frontend/app.js
// 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.

app/main.py
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.

app/main.py
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.

tests/test_cors.py
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

IssueCauseSolution
No Access-Control headerCORS middleware not configuredAdd CORSMiddleware to app
Wrong origin in headerOrigin header doesn't match allowed listAdd origin to allow_origins list
Preflight failsRequest method or headers not allowedAdd method/header to allow_methods or allow_headers
Credentials not sentallow_credentials=FalseSet allow_credentials=True
Wildcard with credentialsallow_origins=['*'] and allow_credentials=TrueUse specific origins with credentials

10Production CORS Configuration

Production CORS should be restrictive and environment-aware.

app/main.py
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.

ScenarioAlternativeWhen to Use
Internal microservicesAPI Gateway with mTLSService-to-service communication
Mobile appsToken authenticationCORS is a browser rule — native apps skip it
Public APIOAuth2 + CORSThird-party applications
Same domainNo CORS neededFrontend and API on same domain
Limited accessWhitelist specific IPsServer-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.

app/main.py
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!

About the Author

TG

Thirdy Gayares

Passionate developer creating custom solutions for everyone. I specialize in building user-friendly tools that solve real-world problems while maintaining the highest standards of security and privacy.