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

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

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.

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

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

bash
# 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.

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

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

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

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 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.

bash
# 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 appsAPI Keys in headersNative apps cannot be spoofed
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

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

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.