The short answer
Quick answer: Browsers enforce a same-origin policy: JavaScript on one site cannot read responses from a different site. CORS (Cross-Origin Resource Sharing) is the mechanism a server uses to relax that rule for chosen sites, by sending response headers such as Access-Control-Allow-Origin. If your page at app.example.com calls api.example.com and the API does not send headers that permit your origin, the browser withholds the response and reports a CORS error. CORS is enforced by the browser, and it is configured on the server. It cannot be fixed from front-end code, and it does not block tools such as curl.
What counts as an origin
An origin is the combination of scheme, host and port. All three must match.
| URL | Same origin as https://app.example.com? |
|---|---|
https://app.example.com/settings | Yes: only the path differs |
http://app.example.com | No: different scheme |
https://api.example.com | No: different host |
https://app.example.com:8443 | No: different port |
https://example.com | No: different host |
Subdomains are separate origins. So are localhost:3000 and localhost:8080, which is why CORS errors are so common in development.
Why the same-origin policy exists
Browsers attach your cookies automatically to requests for the site that set them. Now imagine there were no restriction:
- You are logged in to your bank.
- In another tab, you open a malicious page.
- Its script requests
https://bank.example/accounts. Your browser includes your bank cookies. - The script reads the response and sends your account details elsewhere.
The same-origin policy stops step 4. A script may only read responses from its own origin.
One subtlety matters a great deal: for simple requests the policy blocks reading the response, not sending the request. The request still reaches the server. That is why protection against forged requests (CSRF) is a separate concern; see cookies vs localStorage vs sessions.
The policy has always allowed some cross-origin embedding: images, stylesheets, scripts and iframes. What it restricts is reading data from script, mainly through fetch and XMLHttpRequest.
CORS: the server grants permission
Modern applications need cross-origin calls: a front end on one domain, an API on another. CORS lets the server say which origins may read its responses. MDN's CORS guide is the reference.
Simple requests
For basic requests, the browser sends the request with an Origin header:
GET /products HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
The server replies with a header stating which origin may read the response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
The browser compares. If the header matches the page's origin, or is *, the script gets the response. If it is missing or different, the browser blocks access and logs an error.
A request counts as "simple" only if it uses GET, HEAD or POST, sets no custom headers, and has a Content-Type of a form submission or plain text. These are the requests an ordinary HTML form could already make.
Preflighted requests
Anything else, such as PUT or DELETE, a JSON body, or an Authorization header, could have side effects on servers written before CORS existed. So the browser asks first with an automatic OPTIONS request called a preflight:
OPTIONS /orders/42 HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization, content-type
The server answers with what it allows:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 86400
Only if the answer permits it does the browser send the real request. Access-Control-Max-Age lets the browser cache the answer, so it does not preflight every call.
Note that sending JSON with Content-Type: application/json triggers a preflight. That surprises many people.
The headers
| Response header | Purpose |
|---|---|
Access-Control-Allow-Origin | Which origin may read the response: one specific origin, or * |
Access-Control-Allow-Methods | Which HTTP methods are permitted (preflight) |
Access-Control-Allow-Headers | Which request headers are permitted (preflight) |
Access-Control-Allow-Credentials | Whether cookies and credentials may be included |
Access-Control-Expose-Headers | Which response headers scripts may read |
Access-Control-Max-Age | How long to cache the preflight result |
Credentials
By default, cross-origin fetch calls do not send cookies. To include them:
fetch("https://api.example.com/me", { credentials: "include" });
and the server must respond with:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Two strict rules apply:
- With credentials,
Access-Control-Allow-Origincannot be*. It must name the exact origin. - When the allowed origin varies by request, send
Vary: Originso caches do not serve one origin's response to another.
Cookies also need suitable SameSite settings to be sent cross-site at all.
Fixing common errors
| Error | Likely cause | Fix |
|---|---|---|
| "No 'Access-Control-Allow-Origin' header is present" | The server sends no CORS headers | Configure CORS on the server for your origin |
| "...header has a value that is not equal to the supplied origin" | The allowed origin does not match exactly | Check scheme, host and port; no trailing slash |
| "Response to preflight request doesn't pass access control check" | The server does not handle OPTIONS | Respond to OPTIONS with the CORS headers and a success status, without requiring authentication |
| "Request header field authorization is not allowed" | Header missing from Access-Control-Allow-Headers | Add it |
| "...must not be the wildcard '*' when the request's credentials mode is 'include'" | Wildcard used with credentials | Return the specific origin and allow credentials |
Other things to check:
- Is it really CORS? If the server returns a
500error, or a proxy returns an error page, the response has no CORS headers, and the browser reports CORS. Look at the Network tab for the actual status. - Redirects in the middle of a cross-origin request can break it.
- A gateway or CDN may be stripping headers or answering
OPTIONSitself.
Most frameworks have middleware for this:
// Express
import cors from "cors";
app.use(cors({
origin: ["https://app.example.com"],
credentials: true,
}));
What does not work
- Adding CORS headers to your request. They are response headers. The server grants permission; the client cannot grant it to itself.
mode: "no-cors". It sends the request but gives you an opaque response that your code cannot read.- A browser extension that disables CORS. It only changes your own browser. Your users will still be blocked.
- Public CORS proxies in production. You would be routing your users' data through someone else's server.
Avoiding CORS altogether
If the browser never makes a cross-origin request, CORS never applies.
- Serve the front end and API from the same origin, with a reverse proxy routing
/apito the back end. - Use the development server's proxy, so the browser talks only to
localhost:3000, which forwards API calls. - Call third-party APIs from your own server. Server-to-server requests are not subject to CORS. This also keeps API keys out of the browser.
CORS is not a security feature for your API
CORS protects users from malicious websites reading their data. It does not protect your API from anyone else.
- Tools such as curl, scripts, mobile apps and other servers ignore CORS entirely.
- Your API still needs authentication and authorisation on every request. See JWT vs sessions.
- CORS does not stop cross-site scripting, where the attacker's code runs inside your own origin.
Misconfiguration can open holes:
- Reflecting any
Originback while allowing credentials lets every website read your users' data. - Allowing the
nullorigin. - Loose pattern matching, such as accepting anything ending in
example.com, which also matchesevil-example.com.
Use an explicit allow-list of origins.
Frequently asked questions
What is a CORS error?
The browser blocked your script from reading a response from a different origin because the server did not send headers allowing it.
Why does the request work in Postman but not in the browser?
CORS is enforced only by browsers. Other clients do not apply the same-origin policy.
What is a preflight request?
An automatic OPTIONS request the browser sends before certain cross-origin requests, to ask the server whether the real request is allowed.
Is Access-Control-Allow-Origin: * safe?
For truly public data with no credentials, yes. It must not be used for endpoints that rely on cookies or return private data.
Conclusion
A CORS error is the browser doing its job: refusing to hand one site's data to another site's script without the server's consent. The fix always lives on the server, in the form of the right Access-Control headers and a proper answer to OPTIONS. Decide which origins really need access, list them explicitly, and remember that CORS protects users, while authentication protects your API.
Related articles
- How Cookies, LocalStorage, and Sessions Differ
- How XSS Attacks Work
- JWT vs Sessions: How Authentication Really Works
- REST vs GraphQL vs gRPC: Choosing an API Style
