You need to know whether a header belongs on the preflight or the real response, and every answer you find covers three of the seven. Here is all of them in one place, with the rules that are easy to get wrong.
The Complete CORS Headers List
Seven response headers, and three request headers the browser sends for you.
| Header | Sent on | Purpose |
|---|---|---|
| Access-Control-Allow-Origin | Both | Which origin may read the response |
| Access-Control-Allow-Credentials | Both | Whether cookies and auth are allowed |
| Access-Control-Allow-Methods | Preflight | Which HTTP methods are permitted |
| Access-Control-Allow-Headers | Preflight | Which request headers are permitted |
| Access-Control-Max-Age | Preflight | How long to cache the approval |
| Access-Control-Expose-Headers | Real response | Which response headers JavaScript may read |
| Vary: Origin | Both | Stops caches mixing up per-origin responses |
That "sent on" column resolves most confusion. Putting Allow-Methods only on your GET handler achieves nothing, because the browser reads it from the preflight.
To see which of these your endpoint returns, run it through our CORS tester.
Access-Control-Allow-Origin
One origin, or a star. Never a list.
Access-Control-Allow-Origin: https://app.example.com
The value must match the requesting origin exactly. Scheme, host and port all count, and the comparison is case-sensitive.
Multiple origins are handled in your code, not in the header: check the incoming Origin against an allowlist and echo the match. Use exact equality, never a substring check.
A star works for public data but cannot be combined with credentials. We cover why in the wildcard and credentials conflict.
Access-Control-Allow-Headers and the Safelist
Every non-safelisted request header must be named here or the preflight fails.
Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-Id
Header names are case-insensitive, so the casing you use does not matter. What matters is that nothing is missing.
Safelisted headers — Accept, Accept-Language, Content-Language, and Content-Type within its allowed values — are always permitted and need no listing.
Authorization is not safelisted, which is why adding a bearer token to a previously working request suddenly introduces a preflight.
Access-Control-Expose-Headers
The one people discover last, usually while debugging pagination.
By default your JavaScript can read only seven response headers, no matter what the server sent: Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified and Pragma.
Anything else is invisible. Your custom X-Total-Count arrives, sits in the response, and returns null when you ask for it.
Access-Control-Expose-Headers: X-Total-Count, X-Rate-Limit-Remaining
Rate-limit headers are the other common casualty. Clients cannot back off politely if they cannot see how much quota is left.
Access-Control-Max-Age and Its Hidden Ceiling
Caches the preflight approval, but browsers overrule your number.
Access-Control-Max-Age: 86400
Chrome caps this at two hours and Firefox at twenty-four, whatever you send. Setting it to a year is silently reduced, not honoured.
The cache is keyed by URL, method and headers together. A different endpoint re-asks, so this never becomes one approval for a whole API.
Worth setting anyway. On a chatty single-page app it removes a lot of round trips for very little effort.
The Wildcards That Fail Without Telling You
A star works in three headers, but only when credentials are off.
Allow-Headers, Allow-Methods and Expose-Headers all accept * in modern browsers. Enable credentials and the star stops being a wildcard — it is read as a literal header named star, which matches nothing.
No warning appears. Your request simply fails as though the header were absent, and the config looks correct on inspection.
In our experience this is the most expensive silent failure in the whole specification. We list values explicitly on any endpoint that uses credentials, and recommend the same to clients.
Wrapping Up
Learning the CORS headers list is mostly learning which ones belong on the preflight and which on the real response. Get that mapping right and most errors explain themselves.
Send them on error responses too. That single habit prevents the most misleading failure mode, where a 500 masquerades as a CORS problem.
Need the concepts rather than the reference? Start with what CORS is, then how preflight requests work. To inspect a live response, use our HTTP headers checker.
