Notes
No 'Access-Control-Allow-Origin' header is present on the requested resource:which response is missing it, and the fix
You call an API from your frontend, the request fails, and the browser console prints this:
Access to fetch at 'https://api.example.com/...' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.Firefox words the same failure as Reason: CORS header 'Access-Control-Allow-Origin' missing. Either way, a request did leave the browser (for a preflighted call, only the OPTIONS preflight). For a plain GET or a form-style POST, your server most likely received it, ran it and answered. The browser then read the response headers, found no Access-Control-Allow-Origin, and refused to hand the response to your JavaScript. Your code only sees a failed request (fetch() rejects with a TypeError), and the reason is printed only in the console. That is also why the same call works in curl or Postman: they are not browsers, so nothing runs this check.
So the fix is always on the server, or in whatever sits in front of it. The useful question is not "how do I enable CORS" but "which response came back without the header". One fetch() can produce up to three kinds of response, and the browser checks every one of them. Below is how to tell them apart, then the causes roughly in the order they tend to show up.
First, find the response that is missing the header
Chrome ends several different situations with the same sentence. The words in front of No 'Access-Control-Allow-Origin' header is present narrow it down, and the status code of the failing response settles it:
- "Response to preflight request doesn't pass access control check": the browser sent an
OPTIONSrequest first, and the answer to that preflight had no header. In the reports linked below, the preflight came back as 405 or 501. MDN defines 405 as a method the target resource does not support (hereOPTIONS) and 501 as functionality the server does not support. A preflight also needs a 2xx status, so adding the header to a 405 is not enough. - "(redirected from 'A')": the redirect itself passed the check, the browser followed it, and the response at the new URL (the one after "Access to fetch at") had no header. A typical case is an API that answers
fetch()with a redirect to a login page (section 03). - Neither phrase: the response for the URL in the message had no header. Check that URL's status with the curl commands further down, or in the Network tab. A 301, 302, 303, 307 or 308 means the redirect itself lacks the header: current Chrome stops at that response and the message does not mention a redirect at all (section 03). A 401, 404 or 500 means that error is the real problem and the missing header is a side effect.
- "Redirect from 'A' to 'B' has been blocked by CORS policy": the wording of older Chrome versions, reported for a redirect that itself had no header.
01
The API never sends the header
The plain case: the API has no CORS configuration, or it was never meant to be called from a page on another origin. If you own the server, add the header to the response with your frontend's exact origin:
Access-Control-Allow-Origin: http://localhost:3000
The value is a single origin, never a list. If several frontends call the API, read the request's Origin header, compare it with an allow-list, and send that same value back, together with Vary: Origin so caches keep the answers apart. Express runs middleware in the order you load it, and error handlers are defined last, so register this before authentication and your routes:
Express, registered before everything else
const allowed = new Set(['http://localhost:3000', 'https://app.example.com']);
app.use((req, res, next) => {
const origin = req.headers.origin;
res.setHeader('Vary', 'Origin');
if (allowed.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
if (req.method === 'OPTIONS') {
// the preflight: answer it here with a 2xx, before auth can reject it
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.statusCode = 204;
return res.end();
}
next();
});
If you do not own the API, there is no fix in the browser. Call it from your own backend, or through a small proxy you control, and let the frontend talk to that instead.
02
The header is there on 200, but not on 401, 404 or 500
The browser checks the header on every response, whatever its status. The Fetch Standard says a response to a CORS request "can use any status" as long as it carries the headers. Setups that add them only on the happy path break in a confusing way: the error appears only on some calls, or only after a while. The usual ways the header goes missing on errors:
- CORS is added after authentication, so a 401 for an expired token leaves without it. Your frontend then gets a network error (status 0 in XHR-based clients) instead of a 401, and never gets the chance to refresh the token.
- An error handler or a framework's default 404 or 500 page builds its own response without the headers your middleware adds.
- nginx:
add_headeronly applies to 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308, unless you add thealwaysparameter.
nginx
# without "always", 4xx and 5xx responses get no header
add_header Access-Control-Allow-Origin "https://app.example.com" always;
Fix the order, or add always, and then fix the actual error the missing header was hiding. curl shows you that status directly (see the checks further down).
03
A redirect sits between you and the real response
The Fetch Standard runs the CORS check on each response before it follows a redirect, so a 3xx without the header stops the request right there. Current Chrome then prints the plain message for the URL you called, with no mention of a redirect, so curl -i on that URL is the quickest way to see the 3xx and its Location header. These redirects come up again and again in the reports:
- A missing trailing slash. The API answers
/firstcolumn/2017-03-01/2017-10-26with a redirect to the same path plus/, and that redirect carries no header. Call the final URL directly. - http to https, or the apex domain to www. Same fix: call the URL the server redirects to, so there is no hop.
- A login redirect. Your backend answers a
fetch()with a redirect toaccounts.google.comor another identity provider. That login page will not send your origin a CORS header, and it is not meant to be fetched at all. If your backend put the header on the redirect, this is the case where the console says "(redirected from". Start the login as a navigation instead: a link, a form, or settingwindow.location.
If the request also needed a preflight, some browsers refuse to follow the redirect at all and say so in a separate message; MDN lists the workarounds for that case.
04
The host or platform decides the headers, not your code
When the response comes from static hosting, object storage or a platform in front of your code, that is where the setting lives:
- Netlify static files: a
_headersfile next toindex.htmlwith a/*rule that adds the header. - Firebase Storage: downloading straight from the browser needs a CORS configuration on the Cloud Storage bucket, set from a
cors.jsonfile withgsutil cors set cors.json gs://<your-cloud-storage-bucket>. - Supabase Edge Functions: you add the headers yourself. The official example answers
OPTIONSwith them and passes the same headers on the success response and on the 400 in itscatchbranch. - Azure App Service: the platform has its own CORS setting. Microsoft's documentation says not to combine it with CORS code in your app: when both are used, App Service CORS takes precedence and your own CORS code has no effect. Use one or the other.
- nginx in front of an app: by default, an
add_headerinside alocationblock replaces all the ones inherited from theserverblock. Adding any unrelated header there silently drops your CORS header for that location. Since nginx 1.29.3,add_header_inherit merge;in thatlocationkeeps the inherited headers as well.
05
Your allow-list has a slightly different origin
If your server echoes the origin only when it is on an allow-list, as the snippet above does, an origin that is not on the list gets no header at all, and you see this exact message. An origin is scheme, host and port, and all three have to match:
http://localhost:3000andhttp://127.0.0.1:3000are different hosts.http://localhost:3000andhttp://localhost:5173are different ports.https://example.comandhttps://www.example.comare different hosts, andhttpagainsthttpsis a different scheme.
If the console says from origin 'null', one common cause is a page opened straight from disk as a file:// URL; a sandboxed iframe or a redirect across origins produces it too. Serve the page from a local dev server instead of adding null to the allow-list, which MDN advises against.
After the fix: the header sent twice
If the header is now added in two layers, for example in the app and again in nginx, the message changes to The 'Access-Control-Allow-Origin' header contains multiple values 'http://127.0.0.1:9000, http://127.0.0.1:9000', but only one is allowed. Browsers accept exactly one origin here. Keep the header in one layer and remove it from the other.
Check the fix outside the browser first
curl sends whatever Origin you give it and prints the raw response headers with -i, so you can look at each response the browser would see:
Terminal
# 1. the actual request
curl -i -H "Origin: http://localhost:3000" https://api.example.com/items
# 2. the error path: a route that does not exist, or no token
curl -i -H "Origin: http://localhost:3000" https://api.example.com/does-not-exist
# 3. the preflight, for PUT/PATCH/DELETE, JSON bodies or an Authorization header
curl -i -X OPTIONS \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type, authorization" \
https://api.example.com/items
- Every response contains
Access-Control-Allow-Origin: http://localhost:3000. Header names are case-insensitive, so curl may print it in lowercase. - The preflight returns a 2xx status, not 404 or 405.
- No response is a 301, 302, 303, 307 or 308 with a
Locationheader. If one is, call that location directly. - The header appears exactly once in each response.
Then reload the page with the Network tab open, select the request, and read its response headers there. If curl shows the header and the browser does not, the browser is getting a different response: a cached copy, or one from another layer.
Things that look like fixes but are not
mode: 'no-cors'. The error goes away, but the response becomes opaque: its headers and body are not available to JavaScript. You trade an error for an empty response.- Setting
Access-Control-Allow-Originon the request. It is a response header, so sending it from the client does nothing useful. Worse, it is not a CORS-safelisted request header, so it turns a simple request into one that needs a preflight. Access-Control-Allow-Origin: *with cookies. The wildcard does not work for requests sent with credentials, so you swap this error for another one. Send the exact origin plusAccess-Control-Allow-Credentials: true.- A browser extension or flag that turns CORS off. It changes only your own browser. Your users still get the error.
- A cached response without the header. If the header depends on the origin and responses lack
Vary: Origin, a copy cached from a normal page load, which had no header, can be reused for yourfetch(). SendVary: Origin, or send the same static header on every response.
Check it in 30 seconds
Paste the console error, or the page origin, the request URL and the response headers you see, into CORS Doctor. It works out whether the request needed a preflight, compares the returned origin byte for byte with the page origin, and flags the cases on this page: no header at all, a header missing because the route returned a 4xx or 5xx, localhost against 127.0.0.1, a header sent twice, and a redirect during a preflighted request. The check runs in your browser; it does not call your API.
Questions
Why does the same request work in Postman or curl?
CORS is enforced by the browser. Postman and curl are not browsers, so they show you the response whatever its headers say. That also makes them the right tools for checking what the server really sends.
Can I fix a missing Access-Control-Allow-Origin header from the frontend?
No. The response has to carry the header, so the fix belongs to the server, to a proxy you control, or to the platform in front of the API. mode: 'no-cors' only gives you an opaque response whose body JavaScript cannot read.
Is Access-Control-Allow-Origin: * safe to use?
For a public API that does not use cookies or other credentials, the wildcard is allowed. MDN advises never to use it for private APIs, and it does not work for requests sent with credentials at all.
Did my server run the request even though the browser blocked it?
For a simple request, yes: the browser sends it and only withholds the response, so a POST may already have written to your database. If a preflight was needed and failed, the actual request is never sent.
Why does the CORS error appear only on some requests?
Because only some responses lack the header: a 401 after a token expires, a 404 or 500 from an error path, a redirect, or an origin that is not on your allow-list. Compare a failing request with a working one in the Network tab; the status code or the origin will differ.
Sources
- MDN, "Reason: CORS header 'Access-Control-Allow-Origin' missing": developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS/Errors/CORSMissingAllowOrigin
- MDN, "Access-Control-Allow-Origin" (single origin, allow-list, Vary: Origin, wildcard and credentials, null): developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Origin
- MDN, "Reason: Multiple CORS header 'Access-Control-Allow-Origin' not allowed": developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS/Errors/CORSMultipleAllowOriginNotAllowed
- MDN, "Cross-Origin Resource Sharing (CORS)" (simple requests, preflighted requests and redirects, errors not visible to JavaScript): developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
- MDN, "Request: mode property" (no-cors gives an opaque response): developer.mozilla.org/en-US/docs/Web/API/Request/mode
- MDN, "Origin" (scheme, hostname and port): developer.mozilla.org/en-US/docs/Glossary/Origin
- MDN, "Origin header" (when the origin is null): developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Origin
- WHATWG Fetch Standard, "CORS protocol" and "HTTP responses" (any status, preflight needs an ok status): fetch.spec.whatwg.org/#http-cors-protocol, fetch.spec.whatwg.org/#http-responses
- WHATWG Fetch Standard, "HTTP fetch" (CORS check on each response before a redirect is followed, no actual request after a failed preflight) and "redirect status" (301, 302, 303, 307, 308): fetch.spec.whatwg.org/#http-fetch, fetch.spec.whatwg.org/#redirect-status
- Chromium source,
CorsURLLoader::OnReceiveRedirect(the CORS check runs on the 3xx response and a failure ends the request before the redirect reaches the page): source.chromium.org/chromium/chromium/src/+/main:services/network/cors/cors_url_loader.cc - Chromium source,
GetErrorStringForConsoleMessage(adds "(redirected from ...)" only when the last request URL differs from the first): source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/loader/cors/cors_error_string.cc - MDN, "405 Method Not Allowed" and "501 Not Implemented": developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/405, developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/501
- WHATWG Fetch Standard, "CORS-preflight fetch", "CORS check" and "fetch() method" (TypeError on a network error): fetch.spec.whatwg.org/#cors-preflight-fetch, fetch.spec.whatwg.org/#cors-check, fetch.spec.whatwg.org/#fetch-method
- WHATWG Fetch Standard, "CORS protocol and HTTP caches" (Vary: Origin): fetch.spec.whatwg.org/#cors-protocol-and-http-caches
- nginx, "ngx_http_headers_module: add_header" (status codes, the always parameter, inheritance) and "add_header_inherit" (1.29.3): nginx.org/en/docs/http/ngx_http_headers_module.html#add_header, nginx.org/en/docs/http/ngx_http_headers_module.html#add_header_inherit
- Express, "Writing middleware" (middleware loaded first is executed first) and "Error handling" (error-handling middleware is defined last): expressjs.com/en/guide/writing-middleware.html, expressjs.com/en/guide/error-handling.html
- Microsoft Learn, "Tutorial: Host a RESTful API with CORS in Azure App Service", FAQ "App Service CORS vs. your CORS": learn.microsoft.com/en-us/azure/app-service/app-service-web-tutorial-rest-api
- Firebase, "Download files with Cloud Storage on Web: CORS configuration": firebase.google.com/docs/storage/web/download-files
- Supabase, "CORS support for invoking from the browser": supabase.com/docs/guides/functions/cors
- RFC 9110, section 5.1 "Field Names" (case-insensitive): rfc-editor.org/rfc/rfc9110#section-5.1
- curl, "curl.1 the man page" (-i, -H, -X): curl.se/docs/manpage.html
- Stack Overflow, why Postman works and the browser does not (with Origin 'null'): stackoverflow.com/questions/20035101
- Stack Overflow, preflight answered with 405: stackoverflow.com/questions/35588699; preflight answered with 501, with Access-Control-Allow-Origin set on the request: stackoverflow.com/questions/43871637
- Stack Overflow, redirect to the trailing-slash URL blocked, in the older Chrome wording: stackoverflow.com/questions/46522749; "(redirected from" on Netlify and the _headers file: stackoverflow.com/questions/62507022; fetch() redirected to the Google login page: stackoverflow.com/questions/72382892
- Stack Overflow, a 401 without CORS headers shows up as status 0: stackoverflow.com/questions/38999235; the header with multiple values: stackoverflow.com/questions/22343384