Your API works. You add cookies for authentication and everything breaks. The console now says the wildcard cannot be used when credentials are included, which sounds like a bug in the browser. It is not — the spec forbids that combination on purpose.
The CORS Credentials Wildcard Error, in One Sentence
You cannot say "any site may read this" and "send the user cookies" at the same time.
These two headers are mutually exclusive:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Browsers reject the pair outright. The response is discarded even though the server sent it and the status was 200.
The fix is to name the origin instead of using a star. Everything below is about doing that safely.
Check what your endpoint currently returns with our CORS tester — it flags this specific conflict.
Why the Access-Control-Allow-Credentials Wildcard Rule Exists
Because the combination would break every authenticated site on the internet.
Credentials mean the browser attaches the user cookies automatically. A wildcard means any page may read the reply.
Put them together and any site your user visits could call your API as that logged-in user and read the response. Their bank balance, their messages, their account settings.
So the browser refuses. It is one of the few CORS rules that protects users rather than merely inconveniencing developers.
Echoing the Origin Without Opening a Hole
Validate first, then echo. Never echo blindly.
const ALLOWED = new Set([
'https://app.example.com',
'https://admin.example.com',
]);
const origin = req.headers.origin;
if (ALLOWED.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Vary', 'Origin');
}
Note the exact-match lookup. We have reviewed real code using startsWith, which happily accepts https://example.com.attacker.io because your domain appears as a substring.
Reflecting the origin with no check at all is functionally a wildcard that also allows credentials — the exact thing the rule exists to prevent.
The Vary Header That Stops Caches Leaking
A dynamic origin value needs Vary: Origin, or a cache will mix responses up.
Your CDN stores responses by URL. If the response contains a header that changes per requester, and you do not say so, one origin gets served the copy cached for another.
The failure looks random. It works for you, fails for a colleague, then swaps around after a deploy.
Vary: Origin
One line, and it removes an entire class of bug that is miserable to reproduce.
Getting withCredentials to Actually Work
Both sides have to opt in, and then the cookie itself has to be configured for cross-site use.
fetch('https://api.example.com/me', {
credentials: 'include',
});
That is the client half. The server half is the allow-credentials header shown above.
Then there is the cookie, which is where people get stuck after fixing the headers:
Set-Cookie: session=abc; SameSite=None; Secure; HttpOnly
SameSite=None permits cross-site sending, and browsers require Secure alongside it. Secure means https only, with localhost as the one exception.
Credentials also cover HTTP auth headers and TLS client certificates, so this rule can bite even when you are not using cookies at all.
The Other Wildcards That Fail Quietly
The same restriction applies to three more headers, and their failure is silent.
Access-Control-Allow-Headers: *Access-Control-Allow-Methods: *Access-Control-Expose-Headers: *
All three accept a star in modern browsers — but only when credentials are off. With credentials enabled, the star is treated as a literal header named star, which matches nothing.
No error appears. Your request just fails as though the header were absent, which is why this one costs so much time.
The full behaviour of each is in our CORS headers reference.
Wrapping Up
The CORS credentials wildcard error is the browser enforcing a rule that protects your users. A star and cookies cannot coexist, and no configuration flag changes that.
Keep an explicit allowlist, compare origins with exact string equality, echo the matched value, and always send Vary: Origin.
Still stuck earlier in the chain? See the missing Access-Control-Allow-Origin fix, or read what CORS is protecting against for the wider picture.
