October 4, 2026 · Yunus Emre Vurgun
What Is CORS and How Do You Fix CORS Errors?
CORS (Cross-Origin Resource Sharing) is the browser mechanism that stops a web page from reading the response to a request sent to a different origin unless the server explicitly permits it. An origin is the combination of scheme, host, and port, so https://app.example.com and https://api.example.com count as different origins. If your fetch() call fails with a "blocked by CORS policy" error, the fix is almost always a server-side response header — Access-Control-Allow-Origin — not a change to your JavaScript.
The same-origin policy: why browsers block cross-origin reads
To understand CORS you first need the same-origin policy, the rule every browser enforces: a script running on one origin can only read responses from that same origin. The policy exists because browsers automatically attach cookies and credentials to outgoing requests. Without it, a malicious page at evil.example could silently call your bank's API with your login cookie and read your balance, transaction history, or CSRF tokens straight out of the response.
Note an important subtlety: the same-origin policy blocks reading the response, not sending the request. A cross-origin POST from a plain HTML form has always been possible — that is exactly what cross-site request forgery exploits. What the policy prevents is a script inspecting what came back. CORS is the controlled escape hatch: it lets a server declare, via response headers, that it consents to having its responses read by pages from other origins.
Simple requests — plain GET and POST calls with standard headers and common content types — go straight to the server, and the browser only checks the CORS headers on the way back. Anything more exotic, such as a request with an Authorization header, a JSON body with Content-Type: application/json, or a method like PUT or DELETE, triggers a preflight first. If you need a refresher on which methods exist and what they mean, see HTTP methods: GET, POST, PUT, PATCH, DELETE.
How a preflight request works
A preflight is an automatic OPTIONS request the browser sends before the real request to ask the server for permission. Your JavaScript never sees it; it happens entirely between the browser and the server. The preflight carries two headers describing the intended call: Access-Control-Request-Method names the method, and Access-Control-Request-Headers lists any non-standard headers the real request will include.
The server answers with the CORS policy that applies: which origins may read responses, which methods are allowed, and which headers may be sent. If the browser's planned request fits inside that policy, it proceeds with the real request; otherwise it fails with a CORS error and your fetch() promise rejects. You can reproduce a preflight by hand with curl:
curl -i -X OPTIONS "https://api.example.com/users" \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization"
A healthy server responds with HTTP/2 204 and headers like Access-Control-Allow-Origin: https://app.example.com, Access-Control-Allow-Methods: GET, POST, OPTIONS, and Access-Control-Allow-Headers: Content-Type, Authorization. The browser caches that verdict for the number of seconds in Access-Control-Max-Age, so it does not pay for a preflight on every single call. Preflights are part of how browsers gate the capabilities they expose to scripts, alongside the interfaces documented in the browser web APIs reference.
The CORS response headers you actually need
There are only a handful of headers that matter in practice. Configure these on your API and nearly every CORS problem disappears:
| Header | Purpose |
|---|---|
Access-Control-Allow-Origin | The origin permitted to read the response, or * for any origin on credential-less requests. |
Access-Control-Allow-Methods | Comma-separated methods the browser may use, for example GET, POST, PUT, DELETE, OPTIONS. |
Access-Control-Allow-Headers | Request headers the browser may send, such as Content-Type and Authorization. |
Access-Control-Allow-Credentials | Set to true when the frontend sends cookies or uses fetch with credentials: "include". |
Access-Control-Max-Age | How long, in seconds, the browser may cache the preflight result. A day (86400) is common. |
Access-Control-Expose-Headers | Extra response headers your JavaScript is allowed to read, such as X-RateLimit-Remaining. |
The single most common misconfiguration is combining Access-Control-Allow-Origin: * with credentials. Browsers reject that combination outright: when credentials are involved, the server must echo back the exact requesting origin instead of a wildcard.
Why is my fetch request blocked by CORS?
This is the question behind most CORS searches, and the answer is usually one of five server-side mistakes. First, the response simply lacks Access-Control-Allow-Origin — perhaps CORS was never configured, or it is only configured for some routes and your call hits a different one. Second, the origin is wrong: the header names https://app.example.com but you are testing from http://localhost:3000, and those are different origins, scheme and port included.
Third, credentialed requests fail because the server returns a wildcard origin or forgets Access-Control-Allow-Credentials: true. The client side must also opt in with credentials: "include", and both halves have to agree. Fourth, a custom header is missing from Access-Control-Allow-Headers — the classic case is sending Authorization: Bearer ... while the server only allows Content-Type. Fifth, the preflight itself errors out: the OPTIONS route returns a 404 or 500, or an authentication middleware rejects it before the CORS middleware runs. Always make sure preflights are answered before auth checks.
To debug, open the failing request in your browser's network tab and inspect the preflight exchange directly: the request headers show what the browser asked for, and the response headers show what the server granted. The gap between the two is your bug. Resist the urge to "fix" CORS with a browser extension or by disabling web security — those only hide the problem on your own machine.
CORS checklist for API developers
If you ship an API that browsers will call, treat CORS as part of the API contract, not an afterthought. Echo the requesting origin when it appears on your allow-list rather than hard-coding one value, and always handle OPTIONS on every route that browsers may call. Keep the allow-list tight: every extra origin is another site that can make credentialed requests on your users' behalf. Set a generous Access-Control-Max-Age so repeat visitors skip the preflight round trip.
A minimal Express setup shows the whole pattern — validate the origin, answer preflights, and declare methods and headers:
const ALLOW = new Set(["https://app.example.com"]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOW.has(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Vary", "Origin");
}
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (req.method === "OPTIONS") return res.sendStatus(204);
next();
});
Note the Vary: Origin header: without it, a cache can serve a response carrying one origin's CORS headers to a different origin. Finally, verify your configuration from the outside with curl preflights, not just from your own frontend, and document the policy alongside the rest of your API surface — the agent-friendly HTTP checklist covers the neighboring headers worth getting right at the same time.