What Is CORS? 7 Things Every Developer Should Know (With Preflight Examples)
October 15, 2026 · Web Development
You wire up a frontend, point fetch() at your API, and the browser answers with red text: "blocked by CORS policy." The API works fine in Postman. The endpoint exists. Nothing in your code is wrong — yet the request fails. What is CORS? Cross-Origin Resource Sharing is the browser mechanism that decides when JavaScript on one origin may read responses from a server on a different origin. This guide covers why the rule exists, what counts as an origin, simple vs preflighted requests, credentials, every CORS header that matters, and how to fix the error for real.
What is CORS?
CORS (Cross-Origin Resource Sharing) is a standard mechanism, defined in the WHATWG Fetch Living Standard, that lets a server tell browsers which origins are allowed to read its responses from JavaScript. It works by adding HTTP headers — Access-Control-Allow-Origin and friends — to responses. The browser enforces the decision; the server only declares its policy.
The most common misunderstanding is that CORS blocks requests. It usually does not. For most cross-origin requests, the browser still sends the request — it blocks your page's JavaScript from reading the response when the server has not opted in. That is why the same API call succeeds in curl or Postman: those tools are not browsers, so CORS never applies to them. If your API returns data to curl but your frontend sees a CORS error, the server is fine — its CORS headers are missing.
Why browsers block cross-origin reads: the same-origin policy
CORS exists because of an older, stricter rule: the same-origin policy. By default, a script running on one origin cannot read responses from another origin. Imagine you are logged into your bank, then open a malicious site in another tab. Without the same-origin policy, that site's JavaScript could silently fetch() your bank's pages and read the responses — including account data — using your logged-in session cookies.
The same-origin policy closes that attack completely, but it also blocks legitimate work: every modern app with a frontend on one domain and an API on another would break. CORS is the controlled exception — a way for a server to say "this specific origin is allowed to read my responses." Think of the same-origin policy as the lock and CORS as the guest list.
What counts as an "origin"?
An origin is the combination of three parts: protocol + domain + port. All three must match exactly, or the origins are different:
https://app.example.comvshttp://app.example.com— different protocol, different origin.https://app.example.comvshttps://api.example.com— different subdomain, different origin (subdomains always count as different).http://localhost:3000vshttp://localhost:8080— different port, different origin. This is why a React app on port 3000 calling an API on port 8000 hits CORS in development.
The browser attaches the calling page's origin to every cross-origin request in an Origin header. The server reads it and decides whether to allow that origin in its response.
How CORS works: simple requests
Some requests do not trigger a preflight. These are called simple requests, and they must meet all of these conditions (per MDN's CORS reference):
- Method is
GET,HEAD, orPOST. - Only CORS-safelisted headers are set manually:
Accept,Accept-Language,Content-Language,Content-Type, andRange(single range only). Content-Type, if present, is one ofapplication/x-www-form-urlencoded,multipart/form-data, ortext/plain— the three types an HTML<form>can produce.- No
ReadableStreambody, and no upload-progress listeners on the request.
The logic: a plain HTML form could already send such a request cross-origin in 1999, so servers must already defend against it (that defense is CSRF tokens). For a simple request, the browser sends the real request immediately, then checks the response headers before handing the data to JavaScript:
GET /resources/public-data/ HTTP/1.1
Host: bar.other
Origin: https://foo.example
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: application/xml
[…XML Data…]
Access-Control-Allow-Origin: * means any origin may read this response. To restrict it to one site, the server echoes that origin instead: Access-Control-Allow-Origin: https://foo.example. If the header is missing entirely, the browser throws the CORS error — even though the server returned a 200.
Preflight requests: the OPTIONS handshake
Anything that is not a simple request gets preflighted: before the real request, the browser sends an OPTIONS request asking permission, and only proceeds if the server approves. A POST with a Content-Type: application/json body is the classic trigger — JSON is not one of the three form types, so the browser preflights:
OPTIONS /doc HTTP/1.1
Host: bar.other
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,x-pingother
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
The preflight carries no body and changes nothing — it is a question. The response answers three things: which origins may proceed, which methods are allowed, and which headers are allowed. Only then does the browser send the real POST. Access-Control-Max-Age tells the browser how long to cache that permission (in seconds) instead of preflighting every request; the default is 5 seconds, and browsers cap it — Firefox at 24 hours (86400s), Chromium at 2 hours (7200s).
CORS with credentials: cookies and auth
By default, cross-origin fetch() sends no cookies. To include them, the client opts in with credentials: 'include' — and the server must cooperate with two rules:
- The response must include
Access-Control-Allow-Credentials: true. Access-Control-Allow-Originmust name the exact requesting origin. The*wildcard is rejected for credentialed requests — the server must echo the origin, typically with aVary: Originheader so caches do not mix up responses for different origins.
This is the setup for session-cookie APIs and same-site frontend/backend pairs. If you see a CORS error only on authenticated calls while public calls work, a wildcard origin paired with credentials is the usual culprit.
The 6 CORS headers that matter
| Header | Sent by | What it does |
|---|---|---|
Origin | Browser (request) | Names the calling page's origin so the server can decide. |
Access-Control-Allow-Origin | Server (response) | Which origins may read the response: * or one exact origin. |
Access-Control-Allow-Methods | Server (preflight) | Which methods (GET, POST, PUT…) are permitted cross-origin. |
Access-Control-Allow-Headers | Server (preflight) | Which request headers (e.g. Authorization, Content-Type) are permitted. |
Access-Control-Allow-Credentials | Server (response) | true permits cookies/auth on cross-origin requests; requires an exact origin, never *. |
Access-Control-Expose-Headers | Server (response) | Which extra response headers JavaScript may read (e.g. X-Total-Count). |
One more worth knowing: Access-Control-Max-Age (preflight cache lifetime). And when a server echoes the request origin, it should also send Vary: Origin so shared caches store a separate copy per origin.
Fixing CORS in real projects
Fix it on the server — that is the only real fix. CORS headers must come from the API, because the browser trusts only the server's answer. Three common setups:
Express (Node.js) — the cors middleware. Never use the default app.use(cors()) (open to every origin) on an API that handles real data; name your origin:
const cors = require('cors');
app.use(cors({ origin: 'https://app.example.com' }));
Flask (Python) — Flask-CORS, same idea:
from flask_cors import CORS
CORS(app, origins=["https://app.example.com"])
Django — django-cors-headers, configured in settings:
CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
Debugging checklist when it still fails: (1) open DevTools → Network, find the failing request (often a red OPTIONS), and check whether Access-Control-Allow-Origin is present in the response headers; (2) compare the Origin request header against what the server allows — a trailing slash or www mismatch fails the match; (3) if the preflight fails, fix the preflight before touching the endpoint; (4) verify with curl -H "Origin: https://app.example.com" -v https://api.example.com/data — curl shows the raw headers with no browser in the way. You can also inspect any live endpoint's headers with our HTTP header checker.
When you cannot change the API, the legitimate escape hatch is a same-origin proxy: your frontend calls your own backend, and your backend calls the API server-to-server (no CORS, since CORS is browser-only). Vite, webpack dev server, and Next.js rewrites all support this pattern. It keeps secrets on the server and removes the cross-origin hop entirely. What you should not ship: a browser extension that disables CORS, or asking users to launch Chrome with security flags off.
5 CORS myths debunked
- "CORS is a server-side firewall." No — the server always answers; the browser decides whether JavaScript sees the answer. Any non-browser client reads everything.
- "POST is blocked by CORS, GET is not." A simple
POST(form-encoded, no custom headers) is sent without a preflight, just likeGET. What triggers a preflight is the shape of the request, not the method alone. - "Adding
*fixes everything." It fixes public, credential-free reads. It fails the moment cookies or auth are involved, and it is the wrong default for any API with user data. - "CORS stops CSRF." It does not. Simple cross-origin requests are still sent — that is exactly what a CSRF attack is. CSRF tokens and
SameSitecookies stop CSRF; CORS does not. - "If it works in Postman, the frontend code is buggy." Postman never enforces CORS. A CORS error with a working Postman call points at missing server headers, not at your
fetch()call.
Try it yourself
curl -H "Origin: https://app.example.com" -v https://api.example.com/data— look forAccess-Control-Allow-Originin the response headers.- Trigger a preflight:
fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })from a page on a different origin, then watch theOPTIONSrequest in DevTools → Network. - Paste any API URL into our HTTP header checker to see which CORS headers it actually returns.
- Read the companion guides: what a REST API is (where CORS errors usually appear), client vs server, HTTP vs HTTPS, and what a webhook is — webhooks are server-to-server, so CORS never applies to them.
CORS is not an error to work around — it is the browser asking the server one question: "Is this web app allowed to read your response?" If the server does not answer with headers, the browser hides the data, even when the API returned a 200. Learn to read the Network tab, fix the headers or proxy through your backend, and this error stops wasting your afternoons. References: MDN — Cross-Origin Resource Sharing (CORS) and the WHATWG Fetch Living Standard. Next, read the HTTP status codes cheat sheet to decode what the server actually answered.
Frequently asked questions
- What does CORS stand for?
- CORS stands for Cross-Origin Resource Sharing. It is the browser mechanism that lets a web page on one origin (protocol + domain + port) read responses from a server on a different origin — but only when that server explicitly allows it with CORS response headers.
- Why am I getting a CORS error if my API works in Postman?
- Because CORS is enforced by the browser, not the server. Postman, curl, and server-side code never apply CORS, so they always see the full response. The browser blocks your page's JavaScript from reading the response when the server does not send an Access-Control-Allow-Origin header permitting your origin.
- What is a CORS preflight request?
- A preflight is an automatic OPTIONS request the browser sends before a cross-origin request that is not "simple" — for example, a POST with a JSON body or a custom header. It asks the server which methods and headers are allowed. Only if the server approves does the browser send the real request.
- Can I use Access-Control-Allow-Origin: * with cookies?
- No. When a request includes credentials (cookies, HTTP authentication), the server must return the exact requesting origin in Access-Control-Allow-Origin and include Access-Control-Allow-Credentials: true. The * wildcard is rejected for credentialed requests.
- Does CORS protect my API from attackers?
- Only from browser-based JavaScript reading responses. CORS does not stop anyone from calling your API with curl, Postman, or a server. It also does not stop CSRF: simple cross-origin requests are still sent. Real API protection needs authentication, authorization, and CSRF tokens — CORS is one layer, not the whole wall.
- How do I fix "blocked by CORS policy" in development?
- The real fix is server-side: add the CORS headers on the API, or proxy the API through your dev server so the browser sees a same-origin request (Vite, webpack dev server, and Next.js all support this). Browser extensions that disable CORS are a debugging shortcut, never a fix to ship.
- What is the difference between CORS and the same-origin policy?
- The same-origin policy is the browser's default rule: scripts on one origin cannot read responses from another origin. CORS is the controlled exception — the mechanism by which a server tells the browser "this origin is allowed to read my responses." Same-origin policy is the lock; CORS is the guest list.
- Why can my JavaScript not read a custom response header like X-Total-Count?
- Because browsers only expose a small safelist of response headers to cross-origin JavaScript (Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma). To expose anything else, the server must list it in Access-Control-Expose-Headers.
Related articles
What Is a Proxy Server? 7 Things Every Developer Should Know (With Examples)
What is a proxy server? Learn how proxy servers work for developers — forward vs reverse, HTTP vs SOCKS, CONNECT tunneling, and proxy vs VPN. With examples.
Web DevelopmentWhat Is an API Gateway? 7 Things Every Developer Should Know (With Examples)
What is an API gateway? Learn how API gateways work for developers — routing, auth, rate limiting, aggregation, and when not to use one, with code examples.
Web DevelopmentWhat Is a Load Balancer? 7 Things Every Developer Should Know (With Examples)
What is a load balancer? Learn how load balancers work — Layer 4 vs Layer 7, routing algorithms, health checks, and sticky sessions, with config examples.
Try the free tool
HTTP Header Checker
See a URL's response headers and which common security headers are missing.
Checked server-side — nothing is stored Data & Format ConvertersURL Encoder/Decoder
Encode and decode URLs with correct UTF-8 handling, live conversion, and double-encoding warnings.
Runs entirely in your browser