Complete DevOps Bootcamp: Master DevOps in 12 Weeks
FastAPIMiddleware Security and CORS

CORS in FastAPI

CORS is the most-Googled FastAPI topic that isn't a bug. Almost every developer building a frontend hits the same wall:

Access to fetch at 'http://localhost:8000/api/x' from origin 'http://localhost:5173' has been blocked by CORS policy.

The frustrating part is that this error never came from your code. It came from the browser, refusing to give your JavaScript the response that the server already sent. Understanding why matters more than memorizing the fix.

What CORS actually is

CORS - Cross-Origin Resource Sharing - is a browser security rule. The default behavior is: JavaScript running on https://a.com cannot read responses from https://b.com. CORS is the mechanism that lets a server opt in to allowing this, by sending specific response headers.

The server isn't blocking anything. The browser is. CORS headers are how the server tells the browser "yes, it's fine, this script is allowed to see this response."

An origin, defined

An origin is three things together: scheme + host + port.

URLOrigin
https://api.example.com/usershttps://api.example.com
http://localhost:5173/loginhttp://localhost:5173
http://localhost:8000/apihttp://localhost:8000

Notice the last two are different origins even though both are localhost. Different port = different origin. This is why a Vite frontend on :5173 calling a FastAPI backend on :8000 triggers CORS.

The two-phase dance

Some CORS requests trigger a preflight - an extra OPTIONS request the browser fires before the real one. It happens when the request is "non-simple": custom headers, PUT/DELETE/PATCH, JSON content type, and so on. Basically everything a real API does.

Frontend (https://app.com)          Backend (https://api.com)
        │                                  │
        │  OPTIONS /users                  │
        │  Origin: https://app.com         │
        │  Access-Control-Request-Method:  │
        │      POST                        │
        │ ───────────────────────────────► │
        │                                  │
        │  204 No Content                  │
        │  Access-Control-Allow-Origin:    │
        │      https://app.com             │
        │  Access-Control-Allow-Methods:   │
        │      GET, POST, PUT, DELETE      │
        │ ◄─────────────────────────────── │
        │                                  │
        │  POST /users                     │
        │  Origin: https://app.com         │
        │  { ... json ... }                │
        │ ───────────────────────────────► │
        │                                  │
        │  201 Created                     │
        │  Access-Control-Allow-Origin:    │
        │      https://app.com             │
        │ ◄─────────────────────────────── │

If the preflight fails or the response is missing the right headers, the browser never sends the actual request. The backend logs show nothing. The frontend developer is confused. This is the whole story behind 90% of CORS pain.

The fix

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Five settings to understand. Skim the table, then we'll look at the traps.

SettingWhat it controlsSensible value
allow_originsWhich origins may access this APIExplicit list of your frontends
allow_credentialsWhether cookies / auth headers may be sentTrue only when you actually use cookies
allow_methodsAllowed HTTP methods on the actual request["*"] for simplicity, or list them
allow_headersAllowed custom headers on the request["*"] is fine for most APIs
expose_headersWhich response headers JavaScript can readList the custom ones if you set any

The credentials trap

This is the one that bites everyone exactly once.

# This LOOKS reasonable. It is not.
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,   # ← incompatible with origins="*"
)

The CORS spec forbids the combination of allow_credentials=True and a wildcard origin. The browser will silently refuse the response. You have to name origins explicitly when credentials are in play.

# Correct
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],
    allow_credentials=True,
)

Local dev vs production

You almost always want different settings in development and in production.

import os

if os.getenv("ENV") == "production":
    origins = ["https://app.example.com"]
else:
    origins = [
        "http://localhost:3000",
        "http://localhost:5173",
        "http://127.0.0.1:5173",
    ]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Note localhost and 127.0.0.1 are different origins. Listing both saves a lot of head-scratching later.

Subdomain patterns

When you have many subdomains (app.example.com, admin.example.com, widgets.example.com) the middleware also supports a regex.

app.add_middleware(
    CORSMiddleware,
    allow_origin_regex=r"https://.*\.example\.com",
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Use this carefully. A regex of .* matches every origin on the internet - which is the same as ["*"], with all of the credential incompatibility plus none of the obvious-look-at-it-and-cringe value.

Symptoms and what they really mean

SymptomLikely cause
"No 'Access-Control-Allow-Origin' header is present"The middleware isn't installed, or your origin isn't in the list
"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'"allow_credentials=True with allow_origins=["*"]
Preflight gets a 405 Method Not AllowedThe CORS middleware is after a more restrictive one, or not registered at all
Cookie isn't sent on cross-origin requestsNeed credentials: "include" on the fetch and allow_credentials=True on the server and SameSite=None; Secure on the cookie
Works in Postman, fails in browserThat's the whole point - CORS is a browser-only check. Postman doesn't enforce it.

The "works in Postman" one is worth saying out loud: if it works in Postman but fails in the browser, the bug is in your CORS configuration, not in your route.

Two anti-patterns to avoid

Putting other middlewares between CORS and your routes can swallow the preflight OPTIONS requests. The CORS middleware needs to handle them. If your auth middleware tries to authenticate an OPTIONS request and rejects it, the preflight fails. Register CORSMiddleware early.

Disabling CORS "to make it work" by allowing everything. That ships to production and stays there for years. Be explicit about which origins, and prune the list when frontends are decommissioned.

A final note

CORS exists because the alternative is much worse. Without it, any random site you visited could quietly make authenticated requests to your bank, your email, your everything. The friction is the safety feature. Once you've configured it once, the rest is muscle memory.

How is this guide?

Last updated on