</>CodeWithKarani

FastAPI CORS Silently Failing: the allow_credentials Plus Wildcard Trap

Karani GeoffreyKarani Geoffrey5 min read

You added CORSMiddleware to your FastAPI app, set allow_origins=["*"] because you wanted it to just work, tested it in the browser, and it worked. You shipped. Then the front end team turned on cookie authentication, and every request started failing with a CORS error that points at your API. Nothing in your FastAPI logs. Server returns 200. Browser blocks it anyway.

This is the credentials-plus-wildcard trap, and it is nastier than a normal misconfiguration because it passes every test you run before you add credentials. The moment a cookie or an Authorization header comes along, the browser applies a stricter rule and silently throws the response away. The failure is invisible on the server, and for a while the official FastAPI docs even had example code that walked you straight into it.

The CORS spec forbids the wildcard * for origins, methods or headers whenever allow_credentials=True. This is enforced by the browser, not by FastAPI, so your server logs show nothing. Fix it by listing real origins explicitly:

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

If you truly want allow_origins=["*"], then you must set allow_credentials=False. You cannot have both.

What the browser error actually says

There is no HTTP status for this. The response arrives with 200, and the browser refuses to hand it to your JavaScript, logging something like:

Access to fetch at 'https://api.example.com/me' from origin
'https://app.example.com' 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'.

The exact wording differs slightly between Chrome and Firefox, but the phrase must not be the wildcard '*' when the request's credentials mode is 'include' is the one to search for. Note where it appears: the browser console and the Network tab, never your Uvicorn logs.

Why credentials and a wildcard cannot coexist

CORS has two modes, and the difference is the whole story.

For an anonymous cross-origin request, the server may answer Access-Control-Allow-Origin: *, which means "any site may read this response." That is safe because there is no user session attached; the response is the same for everyone.

A credentialed request carries the user's cookies or auth header. If the browser let a response to a credentialed request be shared with *, then any malicious website the user visits could fire a request to your API with the victim's cookies and read the private response. That is cross-site data theft. So the Fetch standard draws a hard line: when the request's credentials mode is include, the response's Access-Control-Allow-Origin must name a single specific origin, and Access-Control-Allow-Credentials must be true. A literal * is rejected. The same rule applies to Access-Control-Allow-Headers and Access-Control-Allow-Methods in a credentialed preflight: they must be explicit lists, not *.

This is a browser-enforced security rule defined in the Fetch standard's CORS protocol. FastAPI is doing nothing wrong. It sent a response; the browser is refusing to let your code see it.

Anonymous request fetch(url) no cookies server: Allow-Origin: * browser: allowed Credentialed request fetch(url, credentials:'include') server: Allow-Origin: * browser: BLOCKED, response discarded The fix: name the origin Allow-Origin: https://app.example.com
The same server config passes for anonymous traffic and fails for credentialed traffic. That is why it slips through testing.

The doc inconsistency that trapped people

Worth flagging because you may have copied it: FastAPI's own CORS tutorial has carried a mismatch between its prose and its example, tracked in an open FastAPI issue. The prose correctly warns that wildcards cannot be combined with credentials, while example code in the wild pairs allow_origins=["*"] with allow_credentials=True. Copy the example, test without a cookie, and it looks fine. The config is a latent bug that only detonates once real credentials flow.

Step 1: Set origins explicitly whenever credentials are on

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

app = FastAPI()

origins = [
    "https://app.example.com",
    "https://staging.example.com",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    allow_headers=["Authorization", "Content-Type"],
)

Listing methods and headers explicitly rather than * also keeps you correct for credentialed preflights, where the spec applies the same no-wildcard rule.

Step 2: Handle many or dynamic origins without a wildcard

If you legitimately have many front-end origins (per-tenant subdomains, preview deploys), do not fall back to *. Use a regex so the middleware still echoes a single specific origin back:

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

With allow_origin_regex, Starlette matches the incoming Origin and reflects that exact value in Access-Control-Allow-Origin, which satisfies the credentialed rule. Keep the pattern tight; https://.* is just a slow wildcard and reflects any origin that asks.

Verification: read the response headers directly

Bypass the browser and inspect what the server sends for a credentialed preflight:

curl -i -X OPTIONS https://api.example.com/me \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: authorization"

A correct response echoes your origin and allows credentials:

access-control-allow-origin: https://app.example.com
access-control-allow-credentials: true

If you instead see access-control-allow-origin: *, or no access-control-allow-credentials at all, the browser will keep blocking. The presence of your specific origin, not a wildcard, is the proof.

What people get wrong

Chasing it in the backend logs. There is nothing to find. The request reached FastAPI and got a normal response; the enforcement is entirely client side. Open the browser Network tab, click the failed request, and read the console message. If you find yourself adding logging to the API to debug a CORS error, stop and look at the browser. This is a cousin of another silent-failure class I covered in FastAPI 422 errors where the answer is in the response body: the information you need is there, just not where you first looked.

Setting credentials: 'include' on the front end while leaving * on the back end and hoping. These two are contradictory by design. Decide whether the endpoint is credentialed. If yes, name origins. If no, drop credentials on both sides and a wildcard is fine.

Adding the CORS headers manually in a middleware or with @app.options on top of CORSMiddleware. You end up with duplicate or conflicting headers, which some browsers reject outright. Configure it in one place. For the broader picture of why the wildcard is the wrong reflex in general, see blocked by CORS policy: stop reaching for the wildcard origin.

When it is still broken

  1. The origin must match byte for byte. Scheme, host and port all count. https://app.example.com and https://app.example.com:443 and http://app.example.com are three different origins. A trailing slash in your list also breaks the match; use the bare origin.
  2. Check for a proxy stripping or rewriting headers. An nginx or CDN layer in front can drop Access-Control-* headers or inject its own. Run the curl above against the API directly and again through the proxy and compare.
  3. Confirm the preflight is even reaching FastAPI. Some setups answer OPTIONS at the proxy. If the OPTIONS never hits your app, the middleware never runs.
  4. If cookies specifically are not being sent, the cookie also needs SameSite=None; Secure to travel cross-site, which is a separate requirement from CORS but produces the same "it works same-origin, fails cross-origin" symptom.

Frequently asked questions

Why does my FastAPI CORS work until I add cookie authentication?
Because the CORS spec only forbids the wildcard * for origins when the request carries credentials. Anonymous requests are allowed with Access-Control-Allow-Origin: *, but a credentialed request requires a single explicit origin. The browser enforces this, so the config passes every test you run before enabling credentials and fails the moment a cookie or Authorization header is sent.
Can I use allow_origins=['*'] with allow_credentials=True in FastAPI?
No. The Fetch standard rejects a wildcard Access-Control-Allow-Origin for credentialed requests, and the browser discards the response. You must either list explicit origins with allow_credentials=True, or keep the wildcard and set allow_credentials=False. You cannot have both.
Why is there no CORS error in my FastAPI logs?
CORS enforcement is entirely client side. FastAPI sends a normal 200 response; the browser then refuses to hand that response to your JavaScript. The error only appears in the browser console and Network tab, never in Uvicorn or FastAPI logs, so debugging on the server is a dead end.
How do I allow many subdomains without using a wildcard?
Use allow_origin_regex in CORSMiddleware, for example allow_origin_regex=r'https://.*\.example\.com'. Starlette matches the incoming Origin and reflects that exact value back in Access-Control-Allow-Origin, which satisfies the credentialed rule while still supporting many origins. Keep the pattern tight so it does not effectively reflect any origin.
#FastAPI#CORS#Starlette#credentials#cookies#browser security
Keep reading

Related articles