Response header
< Access-Control-Allow-Origin: https://app.example.comAccess-Control-Allow-Origin Header: CORS Errors and Fixes
Access-Control-Allow-Origin explained: exact browser errors, why * fails with credentials, why you need Vary: Origin, and fixes for nginx, Express and Django.
- Direction
- Response
- Category
- CORS
- Spec
- Fetch Standard
On this page
TL;DR:
Access-Control-Allow-Originnames the one origin (or*) allowed to read a cross-origin response. Allow several origins by echoing the request’sOriginfrom an allowlist and addingVary: Origin;*never works with cookies orAuthorization.
What the browser checks
A page at https://app.example.com calls https://api.example.com/data. The browser adds Origin, sends the request, and only exposes the response to JavaScript if the reply allows that origin:
GET /data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
Content-Type: application/json
Valid values are one serialized origin (scheme, host, port, no path, no trailing slash), *, or null. Do not allow null: sandboxed iframes and file:// pages send it.
Exact error messages
Chrome:
Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
The 'Access-Control-Allow-Origin' header has a value 'https://other.example.com' that is not equal to the supplied origin.
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'.
Firefox:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://api.example.com/data. (Reason: CORS header 'Access-Control-Allow-Origin' missing). Status code: 200.
(Reason: Credential is not supported if the CORS header 'Access-Control-Allow-Origin' is '*').
(Reason: Multiple CORS header 'Access-Control-Allow-Origin' not allowed).
The last one means two layers (for example the app and nginx) both added the header; remove one.
Diagnose
- Check the actual response, not the browser summary. Replay with the same
Origin:
curl -si https://api.example.com/data -H 'Origin: https://app.example.com' | grep -iE '^(HTTP|access-control|vary)'
- If the request has a custom header, JSON
Content-Type, or a method other than GET, HEAD or POST, the browser sends a preflight first. Test that too; it must return 2xx with the headers:
curl -si -X OPTIONS https://api.example.com/data \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type,authorization' | head -20
- Find who answers. A
Server: cloudflareplusCF-Ray, or aViaorX-Amz-Cf-Idheader, means a CDN or gateway may be answering, caching or stripping. An error page from the proxy (502, 413, 429) never carries your CORS headers, so the console shows a CORS error that is really a 5xx. - Confirm the error responses also carry the header. Many apps add it only in the success path.
See the CORS debug page and preflight page.
Fix by stack
nginx. always is needed or the header is dropped on 4xx and 5xx responses. Handle OPTIONS yourself, and use a map for the allowlist:
map $http_origin $cors_origin {
default "";
"https://app.example.com" $http_origin;
"https://admin.example.com" $http_origin;
}
server {
location /api/ {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Vary Origin always;
add_header Access-Control-Allow-Credentials true always;
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age 600 always;
add_header Vary Origin always;
return 204;
}
proxy_pass http://app;
}
}
add_header in a block replaces the parent’s, which is why the preflight block repeats them. If the upstream also sets the header you get the duplicate error above; use proxy_hide_header Access-Control-Allow-Origin; first.
Cloudflare Workers. Add headers to the response you return, and answer OPTIONS before fetching origin.
AWS. API Gateway REST APIs need CORS headers on both the OPTIONS mock integration and the Lambda response (proxy integration does not add them). S3 buckets use a CORS configuration with AllowedOrigins. CloudFront must forward Origin to the origin (an origin request policy such as the managed CORS-S3Origin) and the cache policy must include Origin in the cache key, or a response cached for one origin is served to another.
Express, Next.js, Django examples are in the code samples above.
Vary: Origin
If you return a different Access-Control-Allow-Origin per request, add Vary: Origin. Without it, a shared cache or the browser’s own cache can store the response generated for origin A and serve it to origin B, whose browser then fails with “is not equal to the supplied origin”. It also explains CORS errors that appear only after a refresh or on the second visit. See Vary.
Credentials and wildcards
With credentials: 'include', cookies and Authorization are only sent and honoured if the response has an exact origin plus Access-Control-Allow-Credentials: true. Wildcards are treated literally in Access-Control-Allow-Headers, Access-Control-Allow-Methods and Access-Control-Expose-Headers for credentialed requests. Never reflect an arbitrary Origin with credentials enabled; that lets any site act as the logged-in user. Cookie-based credentials additionally depend on SameSite being None; Secure for cross-site calls.
Related
In practice
Express.js
const cors = require('cors')
const ALLOWED = new Set(['https://app.example.com', 'https://admin.example.com'])
app.use(
cors({
origin(origin, cb) {
// No Origin header = same-origin or non-browser client
if (!origin || ALLOWED.has(origin)) return cb(null, true)
cb(null, false) // omit CORS headers rather than throwing a 500
},
credentials: true
})
)Next.js App Router
// app/api/data/route.ts
const ALLOWED = new Set(['https://app.example.com'])
function corsHeaders(origin: string | null): HeadersInit {
const h: Record<string, string> = { Vary: 'Origin' }
if (origin && ALLOWED.has(origin)) {
h['Access-Control-Allow-Origin'] = origin
}
return h
}
export async function GET(request: Request) {
return Response.json({ data: [] }, { headers: corsHeaders(request.headers.get('origin')) })
}
export async function OPTIONS(request: Request) {
return new Response(null, {
status: 204,
headers: {
...corsHeaders(request.headers.get('origin')),
'Access-Control-Allow-Methods': 'GET, POST',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '600'
}
})
}Django
# pip install django-cors-headers
# settings.py
INSTALLED_APPS = [..., 'corsheaders']
MIDDLEWARE = ['corsheaders.middleware.CorsMiddleware', ...] # before CommonMiddleware
CORS_ALLOWED_ORIGINS = [
'https://app.example.com',
'https://admin.example.com',
]
CORS_ALLOW_CREDENTIALS = TrueFrequently asked questions
What does Access-Control-Allow-Origin do?
It tells the browser which origin may read a cross-origin response. The server either returns one specific origin, the literal null, or *. If the value does not match the requesting page origin, the browser hides the response from JavaScript and logs a CORS error. The request itself still reached the server.
Can I allow multiple origins in Access-Control-Allow-Origin?
No. The header takes a single origin or *. To support several origins, compare the request Origin against an allowlist, echo back the matching one, and always send Vary: Origin so caches do not serve one origin the response meant for another.
Why does Access-Control-Allow-Origin: * not work with cookies or Authorization?
For credentialed requests (fetch credentials: include, or XHR withCredentials) the Fetch Standard forbids the wildcard. The response must name the exact origin and include Access-Control-Allow-Credentials: true. The same applies to * in Allow-Headers and Allow-Methods.
Why do I still see a CORS error when the header is present?
Usual causes: the preflight OPTIONS response lacks the header or returns a non-2xx status, the header is added only on 200 responses and not on errors such as 401, 404 or 500, a proxy or CDN stripped or cached it, or the value has a trailing slash or wrong scheme or port.
Is Access-Control-Allow-Origin: * a security risk?
Not for public, unauthenticated data. It becomes a problem when an API relies on network position (intranet, localhost) because any website a user visits could read it. Reflecting any Origin back together with Allow-Credentials: true is the genuinely dangerous pattern.
Does CORS protect my server from other websites?
No. CORS is enforced by browsers to protect users. curl, servers and attackers sending requests directly ignore it, and simple cross-origin requests are still delivered to your server. Authenticate and authorize on the server regardless.
Sources
Related
Access-Control-Expose-Headers: Read Headers in fetch()
Fix response.headers.get() returning null on cross-origin fetch. Access-Control-Expose-Headers, safelisted headers, and why * fails with credentials.
Access-Control-Allow-Credentials Header
Learn how Access-Control-Allow-Credentials controls whether browsers expose responses when credentials (cookies, auth headers) are included in CORS requests.
Access-Control-Allow-Headers Header
Learn how Access-Control-Allow-Headers specifies which custom HTTP headers can be used during cross-origin requests in CORS preflight responses.
Access-Control-Allow-Methods Header
Learn how Access-Control-Allow-Methods specifies which HTTP methods are permitted for cross-origin requests in CORS preflight responses.