What Is CORS and How Does It Work?
CORS stands for Cross-Origin Resource Sharing. It is a browser security mechanism that lets a server explicitly grant a web page permission to read responses from a different origin than the one that served the page. If you have ever seen an error like "blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present," you have already met it.
What CORS actually is
CORS is not a feature you turn on in JavaScript. It is a set of HTTP headers that a server sends, and a set of rules that the browser enforces on the page's behalf. The browser is the gatekeeper: it inspects those headers and decides whether your front-end code is allowed to read the response.
To understand why CORS exists, you first need the Same-Origin Policy. An origin is the combination of scheme, host, and port — for example, https://app.example.com:443. Two URLs share an origin only if all three parts match exactly. https://example.com and http://example.com are different origins (different scheme), and so are example.com and api.example.com (different host). By default, the Same-Origin Policy stops a page on one origin from reading data fetched from another. CORS is the official, controlled way to relax that restriction.
Why browsers enforce it
Without the Same-Origin Policy, any site you visited could quietly make requests to your bank, your email, or an internal company API using the cookies already stored in your browser, then read the private responses and ship them off somewhere. The policy prevents that class of attack by isolating origins from each other's data.
CORS provides the escape hatch for legitimate cases. A single-page app on https://app.example.com that needs data from https://api.example.com is a normal, intended pattern. CORS lets the API server opt in to sharing with specific origins, rather than every origin getting access by default.
How a CORS request works
When your code calls fetch() or XMLHttpRequest against a different origin, the browser attaches an Origin header naming the page's origin. The server then decides whether to allow the response by replying with an Access-Control-Allow-Origin header. The flow splits into two cases depending on how "risky" the request is.
Simple requests
A request is considered simple when it uses GET, HEAD, or POST, sends only a short list of safe headers, and (for POST) uses a content type of application/x-www-form-urlencoded, multipart/form-data, or text/plain. For these, the browser sends the real request immediately and then checks the response:
GET /data HTTP/1.1
Origin: https://app.example.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
If the Access-Control-Allow-Origin value matches the page's origin (or is the wildcard *), the browser hands the response body to your JavaScript. If the header is missing or does not match, the request still reached the server, but the browser blocks your code from reading the response.
Preflighted requests
Anything outside the "simple" rules — a PUT or DELETE, a JSON body with Content-Type: application/json, or a custom header like Authorization or X-API-Key — triggers a preflight. Before the real request, the browser sends an OPTIONS request asking permission:
OPTIONS /data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
The server's OPTIONS response declares which methods and headers it permits. Access-Control-Max-Age tells the browser how many seconds it may cache this permission, so it can skip the preflight on repeated calls. Only after a successful preflight does the browser send the real PUT. You can experiment with these response headers using a CORS Headers Builder and look up the meaning of any status code in the HTTP Status Codes reference.
The key response headers
A handful of Access-Control-* headers control everything. Knowing what each one does removes most of the guesswork:
| Header | Purpose |
|---|---|
Access-Control-Allow-Origin | Which origin may read the response. A specific origin, or * for any. |
Access-Control-Allow-Methods | HTTP methods permitted for the actual request (sent in the preflight response). |
Access-Control-Allow-Headers | Which request headers the client may send. |
Access-Control-Allow-Credentials | Set to true to allow cookies and HTTP auth to be included. |
Access-Control-Expose-Headers | Which response headers JavaScript is allowed to read beyond the safe defaults. |
Access-Control-Max-Age | How long the browser may cache a preflight result. |
Credentials and the wildcard rule
One rule trips up almost everyone. If your request includes credentials — cookies, TLS client certificates, or an Authorization header set automatically by the browser — then the server must respond with Access-Control-Allow-Credentials: true and name an exact origin in Access-Control-Allow-Origin. You cannot combine credentials with the wildcard *; the browser will reject the response if you try. On the client side, credentialed requests also require setting credentials: 'include' in your fetch() options.
Because of this, servers that support multiple front-ends usually read the incoming Origin header, check it against an allow-list, and echo back that exact value rather than hardcoding *.
Common pitfalls and how to fix them
Most CORS errors come from a small set of misunderstandings:
- Thinking CORS is a server-to-server problem. It is purely a browser restriction. Tools like cURL, Postman, and your back-end code ignore CORS entirely — the rules only apply to browser-based JavaScript.
- Forgetting to handle the
OPTIONSpreflight. If your server only handlesPOSTorPUTand returns a 404 or 405 forOPTIONS, the preflight fails and the real request never fires. The preflight must return a 2xx status with the right headers. - Adding a custom header without allowing it. Sending
AuthorizationorX-Request-Idturns a simple request into a preflighted one, and the server must list that header inAccess-Control-Allow-Headers. - Reading a response header that is not exposed. JavaScript can only read a small set of response headers by default; anything else must be named in
Access-Control-Expose-Headers. - Confusing a CORS block with a network failure. A blocked response often surfaces as a generic "Failed to fetch." Use your browser's network panel or an HTTP Header Inspector to see whether the server actually returned the headers you expect.
CORS is not authentication or authorization — it does not protect your API from non-browser clients, and it is not a replacement for server-side access control. It complements other browser security mechanisms; for restricting what scripts and resources a page may load, see our Content Security Policy guide. When you are securing tokens passed between origins, the JWT tokens explained article covers how bearer tokens fit alongside CORS rules. Once you understand that the browser enforces it and the server grants it, most CORS errors resolve quickly.
Frequently Asked Questions
CORS protects users, not your server. It stops a malicious website from reading responses from another origin using the visitor's browser and cookies. It does nothing against non-browser clients like cURL or another server, so you still need real authentication and authorization.
That OPTIONS call is the CORS preflight. The browser sends it automatically whenever a request is not 'simple' — for example, when you use Content-Type: application/json or a custom header like Authorization. Your server must answer the OPTIONS request with the right Access-Control-Allow headers before the real request is sent.
Only if the request does not include credentials. The browser forbids combining the wildcard * with Access-Control-Allow-Credentials: true, so any request that sends cookies or auth headers must receive an exact origin in the header instead of *.
No. CORS is enforced only by web browsers running page JavaScript. Server-to-server calls, cURL, Postman, and mobile apps are not subject to it, which is why a request can fail in the browser but succeed everywhere else.
Open your browser's network panel and inspect the actual response headers on the failing request and its preflight. Confirm the OPTIONS request returns a 2xx status and that Access-Control-Allow-Origin, -Methods, and -Headers cover what you are sending. Checking the raw headers with an HTTP Header Inspector quickly shows what the server is really returning.