Your frontend calls an API with fetch, and the Chrome console shows something like this:
Access to fetch at 'https://api.example.com/orders' from origin 'http://localhost:5173'
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.Your JavaScript only sees TypeError: Failed to fetch (Firefox: TypeError: NetworkError when attempting to fetch resource., Safari: TypeError: Load failed). It means the browser received, or tried to receive, a response from a different origin, and that response did not grant your page permission to read it. The fix almost always belongs on the server, not in your fetch call.
Firefox reports the same problem as Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ... (Reason: CORS header 'Access-Control-Allow-Origin' missing).
Quick fix checklist
- Read the rest of the console message: it names exactly which header was missing or wrong.
- Open the Network panel and find the request. If there is an
OPTIONSrequest just before it, that preflight is what failed. - Make the server send
Access-Control-Allow-Originwith your page’s exact origin (scheme, host and port). - For preflights, the server must answer
OPTIONSwith a 2xx status,Access-Control-Allow-MethodsandAccess-Control-Allow-Headers. - If you send cookies or use
credentials: 'include', the server must echo the origin (not*) and sendAccess-Control-Allow-Credentials: true. - In local development, a dev-server proxy makes the API same-origin and removes CORS entirely.
- Do not use
mode: 'no-cors'to read JSON; it cannot work.
Before you start
An origin is the triple of scheme, host and port: http://localhost:5173 and http://localhost:8787 are different origins, as are http:// and https:// versions of the same host. You should be comfortable reading HTTP request and response headers in the DevTools Network panel, and you need access to the server’s code or configuration (or someone who has it). If you do not control the API at all, skip to the proxy section.
Why it happens
Browsers apply the same-origin policy: a page may send requests anywhere, but by default it may not read responses from other origins. Without it, any website you visit could call your bank’s API with your cookies and read the result. CORS (Cross-Origin Resource Sharing) is the opt-in mechanism a server uses to say “this origin may read my responses”.
That explains why curl and Postman work: they are not browsers, they have no page origin to protect, and they do not enforce CORS. The server answered them exactly as it answered the browser. The browser simply refused to hand the response to your script.
Requests fall into two groups:
- Simple requests:
GET,HEADorPOSTwith only CORS-safelisted headers, and aContent-Typeoftext/plain,multipart/form-dataorapplication/x-www-form-urlencoded. The browser sends them directly and checks the response’sAccess-Control-Allow-Origin. - Preflighted requests: anything else, which in practice means
PUT,PATCH,DELETE, a JSONContent-Typeor anAuthorizationheader. The browser first sends anOPTIONSrequest asking permission. If the answer is not a 2xx with the right headers, the real request is never sent.
A preflight looks like this on the wire:
OPTIONS /orders/1 HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-typeAnd a passing answer:
HTTP/1.1 204 No Content
Vary: Origin
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600Credentials change the rules. When the request includes cookies or HTTP authentication (credentials: 'include'), Access-Control-Allow-Origin: * is rejected and Chrome says the value “must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’”. A wildcard plus credentials would let every site on the internet read authenticated responses. The server must echo one specific origin and add Access-Control-Allow-Credentials: true. In this mode * in Allow-Methods and Allow-Headers is also treated literally rather than as a wildcard.
Step-by-step walkthrough
Step 1: Reproduce and read the whole message
Chrome’s message tells you which check failed. The common variants, after the has been blocked by CORS policy: prefix:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Response to preflight request doesn't pass access control check: It does not have HTTP ok status.
Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.
Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.Step 2: Inspect the exchange in the Network panel
Find the failing request. Check whether an OPTIONS preflight precedes it, look at its status, then the response headers. A preflight returning 401 or 404 usually means auth middleware or routing rejects OPTIONS before the CORS code runs. A 500 with no CORS headers means the server crashed and the error page lacks them, so the real bug is the crash.
Step 3: Fix the server
The server code below (Node, no framework) allows two known origins, supports credentials and answers preflights first. Frameworks have equivalents (the cors middleware for Express, @fastify/cors, Spring’s @CrossOrigin), and they all produce these same headers:
import http from 'node:http';
const ALLOWED_ORIGINS = new Set(['http://localhost:5173', 'https://app.example.com']);
http.createServer((req, res) => {
const origin = req.headers.origin;
res.setHeader('Vary', 'Origin'); // the answer depends on Origin, so caches must key on it
if (origin && ALLOWED_ORIGINS.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin); // echo one exact origin, never '*'
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
if (req.method === 'OPTIONS') { // preflight: answer it before any auth check
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '600');
res.writeHead(204);
return res.end();
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ orders: [{ id: 1, total: 4200 }] }));
}).listen(8787, () => console.log('API on http://localhost:8787'));Use an allowlist, not “echo whatever Origin says”: reflecting every origin with credentials enabled is equivalent to having no protection at all.
Step 4: Or remove the cross-origin request in development
If the API is not yours, or you only need it locally, make the browser see one origin. Dev servers can proxy a path to the API. With Vite, for example (illustrative config):
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': { target: 'http://localhost:8787', changeOrigin: true, rewrite: (p) => p.replace(/^\/api/, '') },
},
},
});The page then calls fetch('/api/orders'), which is same-origin; the dev server forwards it server-to-server, where CORS does not apply. In production you need the same arrangement (a reverse proxy or the API under your domain) or proper CORS headers on the API.
Worked scenario
A React app on http://localhost:5173 loads orders from an API on http://localhost:8787 using a session cookie. This browser code worked with a test endpoint and fails against the real one:
// Browser code (illustrative): runs in the page, not in Node
const res = await fetch('http://localhost:8787/orders', {
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();Diagnosis. The Network panel shows an OPTIONS request first: the JSON Content-Type makes this a preflighted request. The preflight returns 204 with Access-Control-Allow-Origin: *. Chrome reports the wildcard-with-credentials message. Someone had “fixed” CORS earlier by adding *, which works only until cookies are involved.
Fix. Change the server to echo the allowlisted origin and send Access-Control-Allow-Credentials: true, as in Step 3. Also drop the Content-Type header from the GET, since a GET has no body; that turns it back into a simple request and saves a round trip, although the server must still send the right headers on the response.
Common mistake
mode: 'no-cors'. Chrome’s message even suggests it, which is why people try it. It does not disable CORS; it asks for an opaque response: res.type is 'opaque', res.status is 0, res.ok is false and the body is empty to your script. The console error disappears and res.json() then fails because there is nothing to parse. no-cors also strips non-safelisted headers, so a JSON Content-Type or Authorization header is not sent at all. It exists for fire-and-forget requests and for caching opaque resources, not for reading APIs.
Browser extensions or disabled web security. They make the error vanish on your machine only. Every user still hits it.
Setting CORS headers on the request. Access-Control-Allow-Origin is a response header. Adding it to your fetch headers does nothing useful, and as a non-safelisted header it actually triggers a preflight.
Verify the behavior
Simulate the browser’s preflight with curl while the server runs. Curl does not enforce CORS, but it shows exactly what the browser will evaluate:
curl -i -X OPTIONS http://localhost:8787/orders/1 \
-H 'Origin: http://localhost:5173' \
-H 'Access-Control-Request-Method: PUT' \
-H 'Access-Control-Request-Headers: authorization, content-type'HTTP/1.1 204 No Content
Vary: Origin
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600Then repeat a GET with -H 'Origin: https://evil.example'. The server still answers 200 with the JSON body but no Access-Control-Allow-Origin header, which proves the point of this whole article: the request was served, and only a browser would block the page from reading it. Finally, reload the real page with DevTools open and confirm the preflight returns 2xx and the console is clean.
Interview exercise
“Our API works in Postman but the browser says it is blocked by CORS. A colleague suggests disabling CORS on the server by setting Access-Control-Allow-Origin: *. Is that right?”
Answer and reasoning
Postman working tells us the server and network are fine; CORS is a browser-enforced read permission, and Postman does not enforce it. So the fix is indeed a response header, but * is the right value only for genuinely public, unauthenticated data such as a public read-only API. If the frontend sends cookies or uses credentials: 'include', the browser rejects * outright, and even without credentials a wildcard lets any site read the responses, which matters if the API sits on an internal network or relies on IP-based trust. The better answer is an allowlist of known origins echoed back exactly, with Vary: Origin so shared caches do not serve one origin’s headers to another, Access-Control-Allow-Credentials: true if cookies are involved, and an OPTIONS handler that runs before authentication so preflights succeed. I would also point out that CORS is not an authorization mechanism: it controls which browser pages can read responses, while the API must still authenticate every request.
Continue learning
Review related topics with the JavaScript interview questions and the JavaScript MCQs. If the request now succeeds but parsing fails, see Unexpected token ‘<’ in JSON, and for cancelling slow cross-origin requests see composing AbortSignals. The definitive references are MDN’s CORS guide, its CORS errors list and the Access-Control-Allow-Origin header reference.