CORS (Cross-Origin Resource Sharing) is a browser mechanism that lets a server use HTTP response headers to tell the browser which other origins are permitted to read its responses, safely relaxing the same-origin policy. By default a browser blocks JavaScript on one origin from reading responses fetched from another origin. CORS is the controlled exception: the server opts in by returning headers like Access-Control-Allow-Origin, and the browser enforces the rules. Understanding it removes most of the confusion behind the dreaded CORS error.
Key takeaways
- CORS relaxes the same-origin policy so approved cross-origin reads are allowed, and it is enforced by the browser, not by servers or command-line tools.
- An origin is the triple of scheme, host, and port; differ on any one and the request is cross-origin.
- Non-simple requests trigger a preflight
OPTIONScall that must be answered before the real request is sent. - The core response header is
Access-Control-Allow-Origin; credentials require echoing a specific origin plusAccess-Control-Allow-Credentials. - A CORS error means the browser blocked your script from reading a response, not that the server failed to respond.
- CORS is not a security boundary for your API; it protects users’ browsers, not your backend.
What problem does CORS solve?
Browsers enforce the same-origin policy: script running on https://app.example cannot read a response from https://api.other.example. This prevents a malicious page from quietly reading your authenticated data on other sites. But legitimate apps constantly need to call APIs on different origins, such as a frontend on one domain talking to an API on another.
CORS is the negotiated permission slip. The server declares, through response headers, which origins may read its data, and the browser honors that declaration. Without CORS, the only cross-origin options would be blunt workarounds; with it, servers grant precise, per-origin access.
What exactly is an origin?
An origin is defined by three components taken together:
- Scheme such as
httporhttps - Host such as
api.example.com - Port such as
443or8080
If all three match, the request is same-origin. If any differ, it is cross-origin. A subtle consequence is that https://example.com and http://example.com are different origins because the scheme differs, and https://example.com and https://example.com:8443 differ by port. A shared parent domain does not make two hosts the same origin.
What is the difference between a simple request and a preflighted request?
The browser splits cross-origin requests into two categories. A simple request is sent directly, and the browser checks the response headers afterward. A non-simple request is preceded by a preflight: an automatic OPTIONS request that asks the server whether the real request is allowed.
| Condition | Simple request | Preflighted request |
|---|---|---|
| Methods | GET, HEAD, POST |
PUT, PATCH, DELETE, others |
| Headers | Only a small safelisted set | Any custom header such as Authorization |
| Content type | Form or plain text types | application/json |
| Extra round trip | No | Yes, an OPTIONS call first |
Most real API calls, which send JSON with an Authorization header, are preflighted. That is why you often see an OPTIONS request in the network tab before your POST.
How does the preflight request work?
Before sending the real request, the browser sends an OPTIONS request describing what it intends to do:
OPTIONS /orders HTTP/1.1
Origin: https://app.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
The server must respond approving the method, headers, and origin:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
If the preflight response approves everything, the browser proceeds with the actual POST. The Access-Control-Max-Age header lets the browser cache the preflight result for the given number of seconds, avoiding an OPTIONS round trip on every call.
Which CORS response headers matter most?
A handful of headers do the heavy lifting:
Access-Control-Allow-Originnames the permitted origin, or*for any origin. It is the header the browser checks to decide whether your script may read the response.Access-Control-Allow-Methodslists methods allowed for the actual request, used in preflight responses.Access-Control-Allow-Headerslists request headers the client may send.Access-Control-Allow-Credentialswhentruepermits cookies and authorization to be included.Access-Control-Expose-Headerslists response headers the client script is allowed to read beyond the default safelist.Access-Control-Max-Agecaches the preflight decision.
For requests with credentials, the browser refuses a wildcard. You must echo the specific requesting origin in Access-Control-Allow-Origin and set Access-Control-Allow-Credentials: true.
Why do I still get a CORS error when the server responds fine?
This is the most common source of confusion. The server may return 200 with a valid body, yet the browser still throws a CORS error and refuses to hand the response to your JavaScript. That is by design: the request succeeded, but the response lacked the header granting your origin permission to read it.
To debug, check these in order:
- Look at the actual response headers in the network panel, not just the status code.
- Confirm
Access-Control-Allow-Originexactly matches your page’s origin, including scheme and port. - If it is a preflight failure, inspect the
OPTIONSresponse for missing method or header approvals. - If you send cookies, verify the origin is specific (not
*) and credentials are allowed on both ends.
Because the browser enforces CORS, the same request from curl or a server-to-server call will succeed without any CORS headers. That confirms the issue is browser policy, not a broken endpoint.
Is CORS a security feature, and what are the pitfalls?
CORS protects users, not your server. It stops a random page from reading responses using the visitor’s credentials. It does not stop anyone from calling your API directly with a script or tool, so it is not a substitute for authentication, authorization, or rate limiting.
Common pitfalls that undermine safety:
- Reflecting any origin blindly: echoing whatever
Originarrives while also allowing credentials effectively opens your authenticated API to every site. Maintain an allowlist. - Using
*with credentials: browsers reject this combination, and trying to force it is a design error. - Treating CORS as access control: enforce real authorization on the server regardless of origin.
- Forgetting
Vary: Origin: when you echo a dynamic origin, addVary: Originso caches do not serve one origin’s headers to another.
Configure CORS deliberately: allow only the origins you control, expose only the headers you need, and keep authentication independent of it.
How do I configure and debug CORS in practice?
On the server, the cleanest approach is a small piece of middleware that inspects the incoming Origin header, checks it against an allowlist you maintain, and emits the matching headers only for approved origins. Do not hand-roll header strings scattered across routes; centralize the logic so every endpoint behaves consistently and the preflight OPTIONS handler stays in sync with the real handlers.
A reliable debugging sequence saves hours:
- Reproduce the request in the browser and open the network panel to see whether a preflight
OPTIONScall happened and what it returned. - Compare the failing request’s
Originbyte for byte against theAccess-Control-Allow-Originvalue, watching for a trailing slash, wrong scheme, or missing port. - Confirm the same request from
curlsucceeds, which isolates the problem to browser policy rather than the endpoint. - For credentialed requests, verify the origin is echoed specifically and that both client and server opt into credentials.
Also remember that redirects can break CORS, because the browser reevaluates the policy at each hop; keep cross-origin endpoints on a stable URL rather than redirecting through another origin.
Frequently asked questions
What is CORS used for?
CORS lets a server explicitly grant browsers permission for pages from other origins to read its responses, relaxing the same-origin policy in a controlled way.
What counts as a different origin?
An origin is the combination of scheme, host, and port. If any of the three differ between the page and the resource, the request is cross-origin.
What triggers a CORS preflight request?
Any request that is not a simple request, such as one using PUT or DELETE, custom headers, or a JSON content type, triggers an OPTIONS preflight.
Why do I get a CORS error even though the server responds?
The browser blocks your script from reading a response that lacks the correct Access-Control-Allow-Origin header, even when the server returns data normally.
Is CORS a server-side or client-side control?
CORS is enforced by the browser but configured by the server through response headers. The server decides which origins, methods, and headers are allowed.
Does CORS protect my server from attackers?
No. CORS relaxes browser restrictions for legitimate cross-origin reads; it is not a firewall and does not stop direct requests from tools that ignore it.
How do I allow credentials with CORS?
Set Access-Control-Allow-Credentials to true and echo a specific origin, never the wildcard, and have the client send credentials explicitly.