Your screen is too small. Please widen your browser window or use a device with a screen width of at least 300px to use this app.
A browser sends your cross origin request and then refuses to let your JavaScript read the answer. That is the default, and CORS is how a server opts out of it. Here is what each message means, what makes a request get sent twice, and why the fix is almost never in your frontend.
A browser will happily send your request to another origin. What it will not do is hand the response to your JavaScript. That is the default every page starts from, and it is the whole of the same origin policy: script running on one origin may read data from that origin and from nowhere else. An origin is a scheme, a host and a port together, so http and https are different origins, and so are two ports on the same machine.
CORS is how a server opts out of that default. The response carries headers naming which origins are allowed to read it, and the browser enforces whatever the server said. The permission is a whitelist, and it belongs to the server. That is the part most people have backwards: there is nothing to configure on your side, because your side is not the one granting anything.
It is worth being precise about what is withheld, because it changes where you look. For a request simple enough to skip the preflight, the call is delivered, the server runs it, and only the reading of the response is refused. The row really was inserted. CORS is not protecting that server from your page. It is protecting a signed in user from a page that has no business reading their data.
Three messages cover almost every case, and they point at three different servers doing three different things. Reading which one you have is most of the diagnosis.
// the server sent no CORS headers at all
Access to fetch at 'https://api.example.com/orders' from origin
'http://localhost:3000' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.
// the server allows someone, just not you
... blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a
value 'https://app.example.com' that is not equal to the supplied origin.
// the OPTIONS failed, so the request you wrote was never sent
... blocked by CORS policy: Response to preflight request doesn't pass
access control check.The first means the server has no CORS configuration at all, or none that applies to this route. The second means it does have one and your origin is not on the list, which is the one that catches people moving between localhost ports. The third is not about your request at all, it is about the OPTIONS the browser sent ahead of it, and it is the reason the request you were debugging never appears in the server log.
One thing none of these give you is a status code. A blocked response is opaque, so the fetch rejects with a bare TypeError and your error handler cannot tell a CORS refusal from a dead network. That opacity is what makes this failure so slow to trace, and it is deliberate: telling the page why it was refused would leak the thing being protected.
Some cross origin requests are sent straight away. Others are announced first, with an OPTIONS request asking whether the real one would be allowed. That announcement is the preflight, and the browser decides on its own whether to send it.
OPTIONS /orders HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: content-type
Access-Control-Max-Age: 600A request skips the preflight only when the method is GET, HEAD or POST, every header on it is CORS safelisted, and the content type is one of three values: a form encoding, multipart form data, or text/plain. Anything else and the browser asks first.
// no preflight: with no Content-Type set, the browser sends text/plain,
// which is on the safelist
fetch(url, { method: "POST", body: data })
// preflight: application/json is not on the safelist, so this same call
// becomes two round trips
fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})This catches almost everyone, because sending JSON is the most ordinary thing an application does and it is enough on its own to turn one request into two. An Authorization header or any custom header starting with X does the same. When the preflight is what fails, the fix belongs on the OPTIONS response rather than on the one you were watching, and Access-Control-Max-Age is what stops the browser asking again on every call.
An open server answers with Access-Control-Allow-Origin set to an asterisk, which means any origin may read this. That works right up until the request starts carrying cookies or an Authorization header, and then no browser will accept it. A credentialed request requires the server to name the calling origin outright and to send Access-Control-Allow-Credentials alongside it.
The rule exists so that a public resource cannot be turned into a private one by accident. The asterisk is a statement that the response holds nothing worth protecting, and a response assembled from the cookies a browser attached is not that. The consequence is that the moment you add authentication to a call that already worked, the CORS setup that was fine yesterday has to become an explicit list, and the error you get says nothing about credentials at all.
This one does not produce an error, which is why it costs an afternoon. A cross origin response only exposes a small safelist of headers to script: cache-control, content-language, content-length, content-type, expires, last-modified and pragma. Everything else is present on the wire, visible in the network tab, and unreadable from code.
// the server sends it
X-Total-Count: 148
// JavaScript still reads nothing
res.headers.get("X-Total-Count") // null
// until the server also sends this
Access-Control-Expose-Headers: X-Total-CountSo a pagination count, an ETag or a Location you set yourself is invisible until the server names it in Access-Control-Expose-Headers. The symptom looks exactly like a server that forgot to send the header, which sends you off to check the wrong side of the connection. Open the network tab, find the header sitting in the response, and you know which of the two problems you have.
Ordered by how close the fix sits to your browser. The first two make the symptom go away on your machine, which is useful and is not the same thing as the call being allowed.
| Approach | Where the fix lives | Reaches teammates and CI | Can reproduce the failure | Best at |
|---|---|---|---|---|
| Dev server proxy | Your frontend config | Only whoever runs the project | No, it removes the cross origin entirely | Getting unblocked in development this afternoon |
| Disabling browser security | One browser on one machine | No | No, it hides the rule rather than answering it | A one off check that the server really is the problem |
| Changing the backend | The server that holds the data | Yes | Only by breaking a service other people are using | The actual fix, when the server is yours to change |
| Fake API |
In the order that rules out the most for the least effort.
The console names it explicitly and says blocked by CORS policy. A failed DNS lookup, a refused connection and a 500 all break a fetch too, and none of them are fixed by anything on this page.
Check the server log, not the network tab. A simple request is delivered and executed even when the response is withheld, so a POST you never saw succeed may have inserted the row anyway.
Point your app at an endpoint whose answer is yours to set, and test the allowed path and the blocked one.
| The endpoint your app is calling |
| Yes, over HTTPS |
| Yes, on demand and without breaking anything |
| Testing the allowed path and the blocked one against the same screen |
The third row is the real answer whenever the server is yours. The first two are worth knowing for what they cost: a proxy makes the request same origin, so it is not testing the thing that broke, and code that works behind it can fail the first time it is deployed somewhere the proxy does not exist. Turning off browser security proves the server is the problem and fixes nothing for anyone else.
The column worth a second look is the fourth. Every approach here is aimed at making the error stop, and none of them help you answer the other question: when a call really is refused in production, does your screen do something sensible, or does it show a spinner forever. That branch needs a refusal you can summon on purpose.
The OPTIONS is a separate row in the network tab from the request you wrote. When it fails, your real request is never sent at all, so looking for it and not finding it is the expected symptom.
Scheme, host and port are all part of an origin, so http and https differ, and so do port 3000 and port 3001. A value with a trailing slash or a path in it is not an origin and will never match.
The moment a request carries cookies or an Authorization header, the wildcard stops being legal and the server has to name your origin outright. Half of the confusing cases are this one.
A response header that is not on the safelist reaches the browser and still reads as null from JavaScript. It looks like the server forgot to send it, which is the wrong thing to go and check.