Who Blocks the Request, and Why
The first misconception about CORS errors: "the server rejected my request." In reality, the browser refuses to hand the response to your JavaScript. Browsers enforce the Same-Origin Policy — scripts cannot read responses from another origin (the protocol + domain + port triple) — and CORS is the mechanism by which a server declares exceptions: "this origin may read me."
That is why the same request succeeds in curl or Postman and fails only in the browser: the policy lives in the browser. And it is why the fix always lives in the server's response headers.
Decoding the Three Classic Error Messages
| Console message (gist) | Meaning | Fix direction |
|---|---|---|
No 'Access-Control-Allow-Origin' header | Server never declared CORS permission | Add the header to server responses |
Response to preflight request doesn't pass | The OPTIONS pre-check was rejected | Make the server answer OPTIONS with 200 + CORS headers |
... must not be the wildcard '*' when credentials mode is 'include' | Wildcard used with cookie-bearing requests | Name the origin explicitly + Allow-Credentials: true |
When Does a Preflight Happen?
Simple requests (GET/POST with standard headers and plain content types) go straight out. But if any of the following applies, the browser first sends an OPTIONS request asking permission:
- Sending
Content-Type: application/json(which covers most real APIs) - Attaching custom headers such as
Authorization - Using PUT, DELETE, or PATCH
In other words: a JSON API with token auth preflights essentially always. The most common server-side mistake is letting auth middleware answer OPTIONS with 401 — preflight handling must run before authentication.
The Server Header Set
| Header | Example value | Role |
|---|---|---|
| Access-Control-Allow-Origin | https://app.example.com | Allowed origin (no * with credentials) |
| Access-Control-Allow-Methods | GET, POST, PUT, DELETE | Methods declared in the preflight response |
| Access-Control-Allow-Headers | Content-Type, Authorization | Headers the client may send |
| Access-Control-Allow-Credentials | true | Permits cookies/credentials |
| Access-Control-Max-Age | 86400 | Caches the preflight verdict (less OPTIONS traffic) |
Whether these headers actually arrive is exactly what the HTTP header analyzer shows. Filtering for the OPTIONS request in DevTools' Network tab gives the same diagnosis.
Fixes to Avoid
- Blanket
*— unless the API is deliberately public, name your origins. It cannot be combined with cookie auth at all. - Disabling browser security —
--disable-web-securityturns off protection for every site in that browser. Shared with a team, it is an incident waiting to happen. - Random CORS proxies — your full request and response transit a third-party server. With auth tokens attached, you are handing over credentials.
The correct development workaround is your dev server's proxy (Vite, webpack-dev-server) making the API appear same-origin; production gets properly configured CORS headers. The broader header landscape is covered in the HTTP headers guide.
FAQ
Why does it work in Postman but fail in the browser?
Same-Origin Policy is a browser security model. Postman and curl are not subject to it — and that difference is itself the diagnostic proof of a CORS issue.
Server logs show the request succeeded. How is that possible?
The server did respond normally; the browser then refused to expose that response to your script. Blocking after receipt is precisely how CORS works.
Does CORS apply on localhost during development?
Yes — localhost:3000 and localhost:8080 differ in port, so they are different origins. A dev-server proxy is the standard answer.