CORS error explained: “No ‘Access-Control-Allow-Origin’ header is present”
Your fetch works in curl and Postman, then the browser says it was “blocked by CORS policy”. Here is what the browser is protecting, and the fix for each message.
What the browser is protecting
An origin is scheme + host + port: https://app.example.com and http://localhost:3000 are different origins, and so are example.com and api.example.com. By default a page may send requests to other origins but may not read the responses. Otherwise any website you visit could quietly read your webmail or bank pages using your logged-in cookies.
CORS (Cross-Origin Resource Sharing) is how a server opts in: it adds Access-Control-Allow-* headers, and the browser checks them before handing the response to your JavaScript. curl, Postman and server-side code don't run this check at all, which is why the same request “works everywhere except the browser”.
The message, and what it means
| Browser says | What is actually wrong | Fix on the server |
|---|---|---|
| No 'Access-Control-Allow-Origin' header is present on the requested resource | The response has no CORS header at all. Often it's an error page (404, 500, a proxy or login page) that skipped your CORS middleware. | Send Access-Control-Allow-Origin on every response, including errors. Check the status in the Network tab first. |
| Response to preflight request doesn't pass access control check: It does not have HTTP ok status | The OPTIONS preflight got a 401, 404, 405 or redirect. | Answer OPTIONS with 204 (or 200) before any auth check. |
| Request header field authorization is not allowed by Access-Control-Allow-Headers | You sent a header the preflight didn't approve. | Access-Control-Allow-Headers: Authorization, Content-Type |
| Method PUT is not allowed by Access-Control-Allow-Methods | Same, for the method. | Access-Control-Allow-Methods: GET, POST, PUT, DELETE |
| The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when the request's credentials mode is 'include' | You send cookies (credentials: 'include') but the server allows *. | Echo the exact allowed origin, add Access-Control-Allow-Credentials: true and Vary: Origin. |
| Redirect is not allowed for a preflight request | The API redirects (http→https, missing trailing slash) before answering. | Call the final URL directly. See redirect codes explained. |
What is a preflight?
For anything beyond a “simple” request, the browser first sends an OPTIONS request asking permission. A request is simple only if it uses GET, HEAD or POST, sends no custom headers, and any Content-Type is text/plain, multipart/form-data or application/x-www-form-urlencoded. So a POST with Content-Type: application/json, or any request with an Authorization header, always triggers a preflight. A good preflight answer looks like this:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 7200
Vary: Origin
Access-Control-Max-Age lets the browser reuse the answer (Chrome caps it at two hours) so it doesn't preflight every call.
Preflight failing with 405, 401 or a redirect? See CORS preflight failed.
Fixes by situation
- It's your API. Add the headers in one place that runs for every response, errors included. In Express that's the
corspackage, in Flaskflask-cors, in nginxadd_header ... always(withoutalways, nginx drops the header on error responses). - Local development. Let your dev server proxy API calls so the browser only ever talks to one origin, for example Vite's
server.proxyor theproxyfield in Create React App. No CORS needed. - It's someone else's API. You can't fix it from the browser. Call it from your own backend instead, or use an API that sends CORS headers (Tooltern's sister site Anvil allows any origin).
A CORS error can also hide a server error: if the API crashes with a 500 or a proxy returns a 502 Bad Gateway, the error page usually lacks CORS headers, so the browser reports CORS instead. Check the Network tab for the real status code.
Things that don't work
mode: 'no-cors'. The error goes away, but you get an “opaque” response with no status and an empty body. Your code can't read it.- Adding
Access-Control-Allow-Originto the request. It's a response header; sending it only adds a custom header and triggers a preflight. - Allowing
*with cookies, or reflecting any origin with credentials. The first is refused by browsers; the second lets every website read your users' data. Keep an allow-list.
Tip: copy the failing response from the Network tab into the JSON formatter. If it isn't JSON at all (an HTML error page, a login form), that's usually the real problem and the CORS message is just the symptom.
Sending cookies or logins across origins? * stops working; see CORS with credentials and why a cookie is not being set.
Only seeing TypeError: Failed to fetch? A checklist for finding the real cause.
Related
TypeError: fetch failed (Node) · React hydration failed · WebSocket connection failed
Questions
Why does my request work in Postman but not in the browser?
CORS is enforced only by browsers. Postman, curl and server code never check Access-Control-Allow-Origin, so they see the response the browser is hiding from your page.
Can I fix a CORS error from the front end?
No. Only the server (or a proxy on your own origin) can grant access. mode: 'no-cors' just gives you an unreadable opaque response.
Is Access-Control-Allow-Origin: * safe?
For public data that needs no login, yes. Never combine it with cookies or other credentials; browsers refuse that, and reflecting arbitrary origins with credentials is a security hole.
Why do I get a CORS error on a 500 error?
Many frameworks add CORS headers only on successful responses. When the server crashes, the error page has no CORS header, so the browser reports CORS instead of the 500. Fix the error and make sure error responses also carry the header.
Published .