Debug guide · you're seeing
Access to fetch at 'https://api.example.com/items' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.Access to XMLHttpRequest at 'https://api.example.com/items' from origin 'https://app.example.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://api.example.com/items. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 200.Origin https://app.example.com is not allowed by Access-Control-Allow-Origin. Status code: 200
No 'Access-Control-Allow-Origin' Header Is Present: Fix CORS
Fix the CORS error 'No Access-Control-Allow-Origin header is present on the requested resource' with working Express, nginx, S3, Workers and Django fixes.
On this page
TL;DR: The response to your cross-origin request had no
Access-Control-Allow-Originheader matching the page’s origin, so the browser hid it from your script. Add the header on the server that owns the resource, on every response including errors and the OPTIONS preflight. The client cannot fix this.
What it means
When JavaScript on https://app.example.com calls https://api.example.com, the browser sends the request with an Origin header and then inspects the response. If the response does not contain Access-Control-Allow-Origin with either that exact origin or * (for requests without credentials), the browser discards the body and raises the error above. The request usually did reach the server. Side effects of a POST may have already happened.
This is the check defined in the WHATWG Fetch Standard. The wording differs by browser, but the cause is the same:
Chrome: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Firefox: (Reason: CORS header 'Access-Control-Allow-Origin' missing). Status code: 200.
Safari: Origin https://app.example.com is not allowed by Access-Control-Allow-Origin. Status code: 200
Fetch API cannot load https://api.example.com/items due to access control checks.
Firefox also prints (Reason: CORS request did not succeed) when there was no HTTP response at all, such as a DNS failure, a TLS error, a blocked mixed-content request or a connection reset. That variant is not a missing header; fix the network failure first.
Who sent it?
The browser generated the message, but the missing header is the fault of whichever layer produced the response you received. Look at that response in DevTools, Network tab, rather than at the console message.
- Response has
Server: nginxor your framework’s header and a normal body: the origin application or its nginx is not adding the header. - Response has
Server: cloudflareand aCF-Rayheader, orX-Amz-Cf-Id(CloudFront),X-Amzn-Trace-Id(API Gateway or ALB): a CDN or gateway answered. A 4xx or 5xx generated by the gateway itself will not carry your application’s CORS headers. - No response at all (status
(failed)or(blocked)): it is not a header problem. Check for mixed content, an ad blocker, a certificate error, or an unreachable host. - Status is a
301or302: a redirect response has no CORS headers, and a preflight is not allowed to be redirected. Call the final URL directly (https, correct host, trailing slash).
Reproduce it outside the browser by sending the Origin header yourself, because many servers only emit CORS headers when Origin is present:
curl -si https://api.example.com/items \
-H 'Origin: https://app.example.com' | grep -iE '^(HTTP|access-control|vary|server)'
A healthy answer looks like this:
HTTP/2 200
access-control-allow-origin: https://app.example.com
vary: Origin
For a preflight, add the method and headers the browser would ask about:
curl -si -X OPTIONS https://api.example.com/items \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type,authorization'
You want a 2xx (204 is typical) with Access-Control-Allow-Origin, Access-Control-Allow-Methods and Access-Control-Allow-Headers that cover what was requested.
Fix it, in order of likelihood
- Confirm which URL is failing. Compare the URL in the console message with the one you think you are calling. A typo in the host,
httpinstead ofhttps, or a missing/apiprefix often lands on a different server or a 404 handler with no CORS configuration. - Add the header on the server that owns the resource, not on the client.
- Make sure it is added to every response: errors (401, 404, 500), redirects, and the OPTIONS preflight. Middleware order matters; the CORS handler must run before auth and body parsing.
- Reflect the request origin from an allowlist rather than hard-coding
*if cookies orAuthorizationare involved, and sendVary: Origin. - If something sits in front (CDN, API gateway, load balancer), check that it forwards the
Originheader and does not cache one origin’s response for another. - Re-test with curl using the
Originheader, then hard-reload the page. Browsers cache preflight results forAccess-Control-Max-Ageseconds (Chromium caps it at 7200 seconds, Firefox at 86400), so a fixed server can still look broken for a while.
Express (cors package)
import express from 'express'
import cors from 'cors'
const app = express()
const allowed = new Set(['https://app.example.com', 'https://admin.example.com'])
app.use(
cors({
origin(origin, callback) {
// Requests with no Origin (curl, server-to-server) are not CORS requests
if (!origin || allowed.has(origin)) return callback(null, true)
callback(null, false) // omit the header; do not throw, or you get a 500 with no CORS headers
},
credentials: true, // sends Access-Control-Allow-Credentials: true
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 600
})
)
// Register routes AFTER cors(), otherwise early errors skip it
app.use('/items', itemsRouter)
app.use(cors(...)) also answers OPTIONS preflights for every route. The cors package sets Vary: Origin itself when the origin is dynamic.
nginx
The classic mistake is add_header without always. Without it, nginx only adds the header to 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308 responses, so your 401, 404 and 502 pages look like CORS failures. A second trap: add_header directives are inherited from the enclosing level only if the current level defines none, so a single add_header inside a location (or if) silently discards the ones set above it. That is why the if block below repeats every header.
map $http_origin $cors_origin {
default "";
"https://app.example.com" $http_origin;
"https://admin.example.com" $http_origin;
}
server {
listen 443 ssl;
server_name api.example.com;
location / {
# An empty value means nginx does not emit the header at all
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Vary "Origin" always;
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, 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_backend;
}
}
If the upstream application already sends CORS headers, do not add them again in nginx. Duplicated Access-Control-Allow-Origin values are rejected by Chrome with a different error (The 'Access-Control-Allow-Origin' header contains multiple values '...', but only one is allowed). Use proxy_hide_header Access-Control-Allow-Origin; before add_header if you want nginx to own the policy.
Cloudflare Workers
const ALLOWED = new Set(['https://app.example.com'])
function corsHeaders(origin) {
const h = new Headers()
if (origin && ALLOWED.has(origin)) {
h.set('Access-Control-Allow-Origin', origin)
h.set('Access-Control-Allow-Credentials', 'true')
}
return h
}
export default {
async fetch(request) {
const origin = request.headers.get('Origin')
if (request.method === 'OPTIONS') {
const h = corsHeaders(origin)
h.set('Vary', 'Origin, Access-Control-Request-Headers')
h.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
h.set('Access-Control-Allow-Headers', request.headers.get('Access-Control-Request-Headers') || 'Content-Type')
h.set('Access-Control-Max-Age', '600')
return new Response(null, { status: 204, headers: h })
}
const upstream = await fetch(request)
// Response from fetch() is immutable; copy it to change headers
const response = new Response(upstream.body, upstream)
for (const [k, v] of corsHeaders(origin)) response.headers.set(k, v)
response.headers.append('Vary', 'Origin') // append: the origin may already send Vary: Accept-Encoding
return response
}
}
Amazon S3 (and CloudFront in front of it)
S3 only emits CORS headers when the request carries an Origin header and a rule matches. In the bucket’s Permissions tab, under Cross-origin resource sharing (CORS), paste a JSON array of rules:
[
{
"AllowedOrigins": ["https://app.example.com"],
"AllowedMethods": ["GET", "HEAD", "PUT"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
With CloudFront in front, the distribution must forward Origin to S3 and include it in the cache key, otherwise CloudFront can cache a response made without Origin (no CORS headers) and serve it to everyone. Put Origin in the cache key with a cache policy that whitelists it, and forward it to S3 with the managed CORS-S3Origin origin request policy. Alternatively, add CORS headers at the edge with a response headers policy and skip S3 CORS.
API Gateway
For an HTTP API, set the CORS configuration on the API (AllowOrigins, AllowMethods, AllowHeaders, optionally AllowCredentials, MaxAge). When it is configured, API Gateway answers preflights itself and applies its own headers to responses. For a REST API with Lambda proxy integration, “Enable CORS” only creates the OPTIONS mock; your Lambda must return Access-Control-Allow-Origin in its own response, and gateway-generated errors (403 from an authorizer, 429 throttling, 5xx) need CORS headers added through Gateway Responses.
Next.js route handlers
// app/api/items/route.ts
const ALLOWED = new Set(['https://app.example.com'])
function cors(origin: string | null) {
const headers: Record<string, string> = { Vary: 'Origin' }
if (origin && ALLOWED.has(origin)) {
headers['Access-Control-Allow-Origin'] = origin
headers['Access-Control-Allow-Credentials'] = 'true'
}
return headers
}
export async function OPTIONS(request: Request) {
return new Response(null, {
status: 204,
headers: {
...cors(request.headers.get('origin')),
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '600'
}
})
}
export async function GET(request: Request) {
return Response.json({ items: [] }, { headers: cors(request.headers.get('origin')) })
}
For a single fixed origin you can instead set headers() in next.config for /api/:path*. It cannot reflect the origin from an allowlist.
Django and Flask
# Django: pip install django-cors-headers
INSTALLED_APPS = [..., 'corsheaders']
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # as high as possible, before CommonMiddleware
'django.middleware.common.CommonMiddleware',
...
]
CORS_ALLOWED_ORIGINS = ['https://app.example.com']
CORS_ALLOW_CREDENTIALS = True
# Flask: pip install flask-cors
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app, origins=['https://app.example.com'], supports_credentials=True, max_age=600)
Credentials and the wildcard rule
If the request uses credentials: 'include' (cookies, client certificates) the response must contain a specific origin, not *, plus Access-Control-Allow-Credentials: true. An Authorization header set manually in JavaScript is not “credentials” in the Fetch sense, but it does force a preflight, and the preflight must list Authorization by name in Access-Control-Allow-Headers: even without credentials, a * there does not cover it (MDN). On credentialed requests, * in Access-Control-Allow-Headers and Access-Control-Allow-Methods is treated as a literal string, not a wildcard. Name each one.
Never reflect an arbitrary Origin value back with credentials enabled. That makes every site on the internet a trusted origin for your logged-in users. Match against an allowlist.
Why it works in Postman and curl
Nothing in the HTTP exchange changes. Postman and curl send the same request and receive the same bytes. They just do not apply the same-origin policy, which exists to protect users’ browser sessions, not your API. A server that “works in Postman” has not proven it is CORS-correct. It has proven it is reachable. The only useful test is to replay the request with the same Origin header and, for non-simple requests, the same preflight.
Related
- Debugging CORS errors covers the other CORS failure messages.
- Failed CORS preflight for
Response to preflight request doesn't pass access control check. - Access-Control-Allow-Origin, Access-Control-Allow-Credentials and Vary for the header semantics.
- CORS guide for the model behind simple versus preflighted requests.
- The CORS debugger lets you test an endpoint with a chosen origin.
Frequently asked questions
Why does my request work in Postman or curl but fail in the browser with a CORS error?
CORS is enforced by the browser, not the server. Postman and curl never check Access-Control-Allow-Origin, so the response is always delivered to you. The browser fetches the response, sees that the header is missing or does not match the page's origin, and refuses to expose the body to your JavaScript. The server was never going to block the request; the browser blocks the read.
Can I fix a CORS error from the frontend?
No. Setting mode 'no-cors' only gives you an opaque response with no readable body or status, and adding Access-Control-* headers to the request does nothing because those are response headers. The fix is on the server that owns the resource, or a same-origin proxy that forwards the request server-side.
Why do I get a CORS error when the server actually returned a 500 or 404?
Error responses frequently skip the middleware that adds CORS headers, for example when an auth check, body parser, or gateway rejects the request first. The browser reports the missing header and hides the real status. Open the Network tab and look at the response itself, or repeat the request with curl including an Origin header.
Can I use Access-Control-Allow-Origin: * together with cookies?
No. When the request is made with credentials (fetch credentials: 'include', or withCredentials = true), the browser rejects a wildcard Access-Control-Allow-Origin and requires the exact origin plus Access-Control-Allow-Credentials: true. The same wildcard restriction applies to Access-Control-Allow-Headers and Access-Control-Allow-Methods on credentialed requests.
Do I need Vary: Origin?
Yes, whenever the Access-Control-Allow-Origin value depends on the request's Origin header. Without Vary: Origin, a shared cache or CDN can store the response for one origin and replay it to another, which produces intermittent CORS failures that depend on which site hit the cache first.
Why does the error appear only after I add an Authorization header or send JSON?
Those make the request non-simple, so the browser sends an OPTIONS preflight first. If the preflight response lacks the CORS headers, or returns a redirect or a non-2xx status, the real request is never sent. See the failed preflight debug guide for that exchange.
Sources
- MDN: Reason: CORS header Access-Control-Allow-Origin missingdeveloper.mozilla.org
- MDN: Cross-Origin Resource Sharing (CORS)developer.mozilla.org
- WHATWG Fetch Standard: CORS protocolfetch.spec.whatwg.org
- RFC 9110 Section 12.5.5: Varyrfc-editor.org
- Amazon S3 User Guide: CORS configurationdocs.aws.amazon.com
Related
Cross-Origin Resource Sharing (CORS)
Master Cross-Origin Resource Sharing (CORS) for secure cross-origin HTTP requests. Learn preflight requests, headers, credentials, and common error solutions.
Access-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.
Vary Header: Cache Keys, Vary: Origin and Accept-Encoding
Vary lists the request headers a response depends on, so caches keep separate copies. Vary: Accept-Encoding, Vary: Origin for CORS, Vary: *, and CDN cache keys.
Request Header Or Cookie Too Large: Fix 400 and 431
Fix 400 Request Header Or Cookie Too Large and 431 errors: find the oversized cookie or token, then check nginx, Apache, Node, ALB and Cloudflare limits.