What is CORS and why do browsers enforce it?
Cross-Origin Resource Sharing (CORS) is a browser mechanism that controls which cross-origin requests are allowed. It exists because of the same-origin policy: a security rule that prevents JavaScript on one origin from reading responses from a different origin.
The same-origin policy protects users. Without it, a malicious page at evil.com could silently make requests to your-bank.com using your cookies, read the response, and exfiltrate your data.
CORS is the controlled relaxation of that policy. It lets servers declare which outside origins they trust by adding specific HTTP response headers. The browser reads those headers and either allows or blocks the cross-origin response.
What counts as "same origin"?
Two URLs share an origin only if all three of these match exactly:
| Part | Example | Cross-origin if different? |
|---|---|---|
| Scheme | https vs http | Yes |
| Host | api.example.com vs example.com | Yes |
| Port | :3000 vs :8080 | Yes |
A common surprise: http://localhost:3000 and http://localhost:8080 are different origins because the port differs. This is why your frontend dev server often hits CORS errors when calling a local API on a different port.
Simple requests vs. preflight requests
Not all cross-origin requests trigger a preflight. The browser sorts them into two categories:
| Aspect | Simple request | Preflighted request |
|---|---|---|
| Methods allowed | GET, POST, HEAD | Any method (PUT, DELETE, PATCH, etc.) |
| Content-Type | application/x-www-form-urlencoded, multipart/form-data, text/plain | application/json and any other type |
| Custom headers | None beyond CORS-safelisted headers | Any custom header (e.g. Authorization) |
| Browser sends OPTIONS first | No | Yes |
| Extra round-trip | No | Yes (cached by Access-Control-Max-Age) |
For a preflighted request, the browser automatically sends an HTTP OPTIONS request to the same URL before the actual request. If the server's preflight response does not include the right CORS headers, the browser aborts the real request entirely.
Most modern API calls (JSON POST with Authorization header) are preflighted. If you see an OPTIONS request in your network tab followed by a CORS error, the preflight is failing.
CORS response headers reference
Access-Control-Allow-Origin
Which origins are allowed. Use a specific origin (https://example.com) or * for public APIs. Cannot be * when credentials are included.
Access-Control-Allow-Methods
Which HTTP methods are permitted for the cross-origin request, e.g. GET, POST, PUT, DELETE.
Access-Control-Allow-Headers
Which request headers the client is allowed to send, e.g. Content-Type, Authorization.
Access-Control-Allow-Credentials
Set to true to allow cookies and authorization headers. Requires a specific origin, not *.
Access-Control-Max-Age
How long (in seconds) the browser can cache the preflight result. Reduces round-trips. Common value: 86400 (one day).
Common CORS errors and how to fix them
No Access-Control-Allow-Origin header
Cause: The server did not include CORS headers in its response.
Fix: Add Access-Control-Allow-Origin to the response on the server. The fix is always server-side.
Credential flag is true but Access-Control-Allow-Origin is *
Cause: You set withCredentials: true on the request, but the server responds with * as the allowed origin.
Fix: Replace * with the specific requesting origin (e.g. https://app.example.com). Wildcards and credentials are mutually exclusive.
Request header not in Access-Control-Allow-Headers
Cause: Your request sends a custom header (e.g. Authorization or X-Api-Key) that the server has not whitelisted.
Fix: Add the header name to Access-Control-Allow-Headers in the preflight response.
Method not in Access-Control-Allow-Methods
Cause: Your request uses a method (e.g. DELETE) that the server has not listed as allowed.
Fix: Add the method to Access-Control-Allow-Methods in the preflight response.
Configuring CORS in Express / Node
The fix for a CORS error is always on the server. Here is a minimal Express middleware that handles both preflight and regular requests:
const express = require('express')
const app = express()
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
res.setHeader('Access-Control-Max-Age', '86400')
if (req.method === 'OPTIONS') {
return res.sendStatus(204)
}
next()
})
For production, use the cors npm package and configure it with a whitelist of allowed origins rather than a static string. Never use * for authenticated endpoints.
Frequently asked questions
Why can't I just disable CORS in my browser?
You can, with browser flags or extensions, but only for your own browser. Your users cannot. The same-origin policy exists to protect them from malicious pages making requests on their behalf (CSRF-style attacks). Disabling it for development is acceptable, but the real fix is always configuring the server correctly.
Does CORS protect the server?
Not really. CORS is enforced by the browser, not the server. Tools like curl or Postman bypass it entirely because they are not browsers. CORS protects users from malicious websites making cross-origin requests using the user's credentials. Actual server security still requires authentication, authorization, and rate limiting.
What is a CORS proxy and should I use one?
A CORS proxy sits between your browser and the target server, adding CORS headers to the response. It works for third-party APIs you cannot configure directly. For your own APIs, always configure the server directly. Public CORS proxies should never be used with authenticated requests since all traffic passes through them.
Why does my API work in Postman but not the browser?
Postman is not a browser and does not enforce the same-origin policy. It sends requests directly without preflight checks or CORS validation. If your API works in Postman but fails in the browser, the API is reachable and responding correctly. The problem is missing CORS headers on the server response.
Can I use Access-Control-Allow-Origin: * with credentials?
No. The spec explicitly forbids it. When a request includes credentials (cookies, HTTP auth, or client certificates), the server must respond with a specific origin, not a wildcard. If you set both, the browser will reject the response even though the server sent it.
What does Access-Control-Max-Age do?
It tells the browser how long to cache the preflight result in seconds. Without it, the browser sends an OPTIONS preflight before every non-simple cross-origin request. Setting a long max-age (e.g. 86400 for one day) eliminates these extra round-trips and noticeably speeds up APIs that receive many cross-origin requests.