22/10/2026 12:00pm

CORS Error Explained: Why Browsers Block It and How to Fix It
#CORS
#CORS error
#CORS policy
#Access-Control-Allow-Origin
#Same-Origin Policy
#Preflight Request
#cors error localhost
Picture this: you have just finished your first page with Vite, running at http://localhost:5173, and you call fetch to load a list of users from the API a teammate is running at http://localhost:3000. You try the same URL with curl and the data comes back fine. But from the page, nothing shows up. You open the Console and find one long red line with the words CORS policy in the middle.
Don't panic. Almost everyone who connects a frontend to an API for the first time hits this exact message at this exact moment. What makes it confusing is that your code looks correct and the server is clearly working. Today Superdev Academy walks through what a CORS error actually is, why the browser blocks it, whether your request ever reached the server, and most importantly where the fix belongs, because it is not in your frontend code, which is where most people start looking.
What the red message is actually telling you
We recreated the scenario above on a real machine with two small Node.js servers: one serving the page on port 5173, and one API on port 3000 with no CORS configuration at all. Opening the page in Google Chrome produced this line in the Console:
Access to fetch at 'http://localhost:3000/api/users' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.Read it in pieces and it tells you everything. The first part says where the request went. The second says which origin your page came from. The last part gives the reason: the server's response did not include a header called Access-Control-Allow-Origin.
Inside your JavaScript, though, the catch block only receives TypeError: Failed to fetch, with no further detail. That is not a bug. MDN's CORS guide explains that the specifics of a CORS failure are deliberately hidden from JavaScript for security reasons. Your code only learns that something failed, and the only place to see why is the browser's Console. So whenever you see Failed to fetch, open the Console before you start rewriting code.
What is an origin, and why is localhost not the same as localhost?
The word that appears most in these errors is origin. MDN defines it as three parts of a URL: the scheme (the protocol, such as http or https), the host (the domain name) and the port. Two URLs share an origin only when all three match. The path after them does not count.
http://localhost:5173andhttp://localhost:3000: different origins, because the ports differhttp://example.comandhttps://example.com: different origins, because the schemes differhttp://example.comandhttp://www.example.com: different origins, because the hosts differhttp://example.com/app1andhttp://example.com/app2: same origin, only the path differs
In plain terms, your page and your API can run on the same laptop under the same name, localhost, and the browser still treats them as two different sites if the ports do not match. During development that is almost always the case, since Vite uses port 5173 by default and the API usually runs on another one.
The Same-Origin Policy: who is the browser protecting?
The rule behind the message is the Same-Origin Policy. According to MDN, it is a security mechanism that restricts how a document or script from one origin can interact with resources from another. Reading data across origins is, by default, not allowed.
MDN's own example makes the reason obvious. Say you are logged into your webmail and open some unknown site in another tab. Without this rule, a script on that site could fetch your webmail, and because the browser attaches your cookies automatically, read your inbox and send it wherever it likes.
A CORS error is not there to stop you. It is there to stop other sites from reading a user's data through that user's own browser.
CORS, short for Cross-Origin Resource Sharing, is how a server relaxes that rule on purpose: it uses HTTP headers to declare which origins may read its responses. Notice who does the declaring. It is the server, not the page. That one fact drives every correct fix in this article.
The request reached the server; the browser just would not hand over the response
The most common beginner assumption is that the browser stopped the request before it left the machine. In our test, the API's log showed this:
[api] GET /api/users Origin=http://localhost:5173So the GET request did reach the server, and the server sent back its full response. The browser then checked that response, found no header allowing our origin, and refused to let our JavaScript read it. The failure is in reading the response, not in sending the request.
That also explains the other puzzle: why curl or Postman works fine. We called the same API like this:
curl -i -H "Origin: http://localhost:5173" http://localhost:3000/api/usersIt returned HTTP/1.1 200 OK with the full JSON body and not a single CORS header. CORS is enforced by browsers; curl and Postman do not enforce it. A request that works in curl proves nothing about whether it will work in a browser. If you want a refresher on requests, responses and status codes first, read HTTP Status Codes Explained alongside this.
What is a preflight, and why does an OPTIONS request show up first?
If the Network tab shows an OPTIONS request going out before your real one, that is a preflight request: the browser asking the server in advance, "if I send this, will you accept it?"
MDN's CORS guide calls requests that skip this step simple requests, and they must meet every condition. For example, the method must be GET, HEAD or POST, and any Content-Type must be one of application/x-www-form-urlencoded, multipart/form-data or text/plain. Step outside those rules, say a POST with Content-Type: application/json or an Authorization header, and the browser sends a preflight first, carrying Access-Control-Request-Method and Access-Control-Request-Headers to describe the real request.
In the same test we sent a JSON POST to the unconfigured API. The server log showed only OPTIONS /api/users, with no POST after it, and the Console said:
... has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.So when a request needs a preflight and the preflight fails, the real request is never sent at all. That is different from the plain GET case, where the request arrives but the response cannot be read. When you see the word preflight in the error, check how your server answers OPTIONS.
Fix it in the right place: headers on the server
Because permission comes from the server, the lasting fix lives in the API, not the page. Here is the same API with CORS configured, written with Node.js's built-in http module so you can see it really is just a few headers:
import http from 'node:http';
http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
res.setHeader('Vary', 'Origin');
if (req.method === 'OPTIONS') {
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.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ users: ['Mint', 'Beam'] }));
}).listen(3000);Access-Control-Allow-Origin: the origin allowed to read the response, set to match your page exactlyVary: Origin: MDN recommends it whenever you name a specific origin, so caches know the response depends on the requesting originAccess-Control-Allow-MethodsandAccess-Control-Allow-Headers: tell the preflight which methods and headers are acceptedAccess-Control-Max-Age: how many seconds the browser may cache the preflight result; 600 in this example
To confirm the setup, simulate the preflight with curl:
curl -i -X OPTIONS \
-H "Origin: http://localhost:5173" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type" \
http://localhost:3000/api/usersWe got HTTP/1.1 204 No Content with Access-Control-Allow-Origin: http://localhost:5173 and the rest of the headers. Reloading the page, both the GET and the JSON POST returned data normally, without changing a single line of frontend code.
In real projects hardly anyone writes these headers by hand; most frameworks ship CORS middleware. To see how middleware sits in the middle of every request, read Go and HTTP Middleware, which lists CORS handling as one of middleware's common jobs. If you have never built an API yourself, start with Creating an API with Node.js and Go.
During local development, Vite's proxy is a valid alternative
Sometimes you do not own the API, or you would rather not touch server code while building the frontend. The other correct option is to stop the browser from crossing origins at all. Vite's documentation describes server.proxy, which makes the dev server forward any request whose path starts with a given prefix to another server.
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
server: {
proxy: {
'/api': 'http://localhost:3000',
},
},
})Your page then calls just fetch('/api/users'), with no domain. The browser sees a request to localhost:5173, the same origin as the page, and Vite passes it on to port 3000 server-to-server, where CORS does not apply. We tried it with Vite 8.3.1 against the API that had no CORS headers at all: the page got its data and the Console stayed clean.
Keep in mind that server.proxy only works while the dev server is running. Once you build for production, you have to answer the question again: will the page and the API share an origin? If not, you still need the server-side headers from the previous section.
Shortcuts that look like fixes but are not
Can't I just disable browser security, or install an extension that turns CORS off?
You can, and the red line really does disappear on your machine. Only on your machine. Every real user opening your site in a normal browser still hits the same error. And a browser with this rule switched off no longer protects you from the webmail scenario above. The shortcut simply moves the problem to launch day.
What about Access-Control-Allow-Origin: * and be done with it?
The * wildcard means any origin, and it is fine for a public API that needs no login. But once a request carries cookies or other credentials, MDN's CORS guide is explicit: the server must not use * and must name a specific origin instead. We tested it with fetch and credentials: 'include' against an API answering *, and Chrome blocked it with:
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'.The same request without credentials went through. So * is not a shortcut for anything with a login. If you need several origins, keep an allowlist on the server, echo back the one that matches the request, and add Vary: Origin.
Summary: what a CORS error is and where to fix it
A CORS error is the browser telling you that a page from one origin tried to read a response from another origin without the server's permission. An origin is the scheme, host and port together, so a different port alone makes it a different origin. The rule exists to protect users, and for a simple request the request usually reached the server anyway; your code just was not allowed to read the answer.
That leaves two correct fixes: set Access-Control-Allow-Origin and the related headers on the server, or use Vite's server.proxy during development. Next time the red line appears, read the reason at the end of the Console message, then run curl with an Origin header to see which headers the server actually sends back. Those two steps tell you where the fix belongs. For the bigger picture of how a page talks to a server, continue with Handling HTTP Requests and Responses.
Follow more articles and tips for people starting out as programmers on our channels.
Facebook: Superdev Academy Thailand — https://www.facebook.com/superdev.academy.th
YouTube: Superdev Academy Channel — https://www.youtube.com/@SuperdevAcademy
Instagram: @superdevacademy — https://www.instagram.com/superdevacademy/
TikTok: @superdevacademy — https://www.tiktok.com/@superdevacademy?lang=th-TH
FAQ: Frequently Asked Questions about This Article
A collection of questions and answers to help you better understand the content of this article.