Access-Control-Allow-Origin
HTTP response header · ACAO
An HTTP response header the server sends to tell the browser which origin is allowed to read the response body. The browser refuses to expose the body to JavaScript unless this header is present and matches (or is the wildcard).
Three valid values
Access-Control-Allow-Origin: *— any origin can read the response. Doesn't work for credentialed requests.Access-Control-Allow-Origin: https://app.example.com— exactly this one origin. The server typically reads the request'sOriginheader and echoes it back if it's on an allowlist.- (header absent) — browser blocks the read. The default.
You can't list multiple origins in this header — only one origin or *. To allow several, the server has to inspect the request's Origin and reflect a matching value.
When you also need Vary: Origin
If your server reflects different Access-Control-Allow-Origin values depending on the request, add Vary: Origin to the response so caches don't serve a value meant for one origin to a different origin. Without it, CDNs and browser caches will silently break CORS for some requests.
Example: doing it right
// Node / Express — reflects the request origin if it's on an allowlist
const ALLOWED = new Set([
'https://app.example.com',
'http://localhost:3000'
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && ALLOWED.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
}
next();
});
Common mistakes
- Sending
*with credentialed requests. The browser refuses the combination. If you need cookies orAuthorizationacross origins, you must echo a specific origin. - Forgetting
Vary: Origin. Caches will serve the wrong value to different origins. Subtle, intermittent failures. - Setting it on the upstream when the browser actually needs it on the preflight. If your real call gets a preflight, the OPTIONS response also needs the header — not just the final GET/POST.
- Trailing slashes and case.
https://example.comandhttps://example.com/are not the same string. Match exactly.