What is CORS? Cross-Origin Resource Sharing explained
Cross-Origin Resource Sharing (CORS) is the mechanism browsers use to allow or block requests between different origins. This guide explains how CORS works, why it exists, common errors, and how to fix them.
On this page
- What is an origin?
- Why browsers block cross-origin requests
- How CORS works
- Simple requests
- Preflight requests
- CORS response headers in detail
- Common CORS errors and how to fix them
- "Access to fetch has been blocked by CORS policy"
- Preflight returning 403 or 404
- Credentials and wildcard
- CORS on static sites and CDNs
- CORS vs JSON-P
- FAQ
- Does CORS affect server-to-server requests?
- Is Access-Control-Allow-Origin: * ever safe to use?
- Does CORS protect against attacks?
- Why does my browser send an OPTIONS request before every POST?
- Verdict
Browsers block web pages from reading data from a different origin — by design. This security rule, called the same-origin policy, prevents one site from reading another site's private data. But it also blocks legitimate uses: a front-end app fetching data from an API on a different domain, a page loading fonts from a CDN, or a JavaScript module importing from a separate host. Cross-Origin Resource Sharing (CORS) is the mechanism that solves this, letting servers explicitly allow cross-origin access when it is intended.
What is an origin?
An origin is the combination of three things about a URL: the scheme (http or https), the host (domain name), and the port (if specified). Two URLs have the same origin only when all three match.
| URL | Origin |
|---|---|
https://example.com/page |
https://example.com |
https://api.example.com/data |
https://api.example.com — different host |
https://example.com:8080/api |
https://example.com:8080 — different port |
https://example.com/page vs http://example.com/page |
Different scheme |
A cross-origin request is any request where the origin of the requesting page differs from the origin of the resource it is fetching. For example, a page at https://myapp.com using fetch('https://api.myservice.com/users') is a cross-origin request.
Why browsers block cross-origin requests
The same-origin policy has been a cornerstone of web browser security since Netscape Navigator 2.0 in 1996. Without it, a malicious page loaded in your browser could read emails from mail.google.com, access your bank account data from bank.com, or extract sensitive information from any site you were logged into.
Browsers enforce this by restricting what scripts running on page A can read from origin B. Both fetch() and XMLHttpRequest follow the same-origin policy. If origin B does not explicitly permit the request, the browser blocks the response — even if the server sent it successfully.
This is important to understand: the server receives the request. The browser is the one blocking the response body from reaching your JavaScript code.
How CORS works
CORS is an HTTP header mechanism. The server receiving a cross-origin request adds headers to its response; the browser reads those headers and decides whether to let the JavaScript code see the response.
There are two types of requests that CORS handles differently.
Simple requests
A simple request meets all three of these conditions:
- Uses
GET,HEAD, orPOST - Uses only safe headers (no custom headers like
AuthorizationorX-Custom-Header) - Sends a Content-Type of
application/x-www-form-urlencoded,multipart/form-data, ortext/plain
For simple requests, the browser sends the Origin header automatically:
Origin: https://myapp.com
If the server allows that origin, it responds with the Access-Control-Allow-Origin header:
Access-Control-Allow-Origin: https://myapp.com
Or to allow any origin:
Access-Control-Allow-Origin: *
If the origin is not allowed, the browser receives the response but blocks JavaScript from reading it, and throws a CORS error in the console.
Preflight requests
For requests that are not simple — for example, a DELETE request, a POST with application/json, or a request with a custom header — the browser first sends a preflight request using the OPTIONS HTTP method.
A preflight asks the server: "Do you actually allow this cross-origin request?" The browser sends two headers:
Origin: https://myapp.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type
The server responds with:
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
Access-Control-Max-Age tells the browser how long (in seconds) it can cache this preflight result, so it does not have to repeat the OPTIONS request for every subsequent call.
Only if the preflight succeeds does the browser send the actual request. If the preflight fails, the actual request is never sent.
CORS response headers in detail
These are the headers a server can send to control cross-origin access:
Access-Control-Allow-Origin— Required. Specifies which origins can access the resource. Use a specific origin (https://myapp.com) or*for public resources.Access-Control-Allow-Methods— Lists permitted HTTP methods for non-simple requests (sent in response to preflight).Access-Control-Allow-Headers— Lists permitted request headers for non-simple requests.Access-Control-Allow-Credentials— Set totrueto allow cookies, authorization headers, or TLS client certificates to be sent cross-origin. Cannot betruewhenAccess-Control-Allow-Originis*.Access-Control-Expose-Headers— Lists response headers (beyond the six safe ones) that JavaScript can read.Access-Control-Max-Age— How long the browser may cache the preflight result.
Common CORS errors and how to fix them
"Access to fetch has been blocked by CORS policy"
The most common CORS error. The browser blocked the response because the server did not include the right Access-Control-Allow-Origin header.
Fix: Add the Access-Control-Allow-Origin header on the server. For a Node.js/Express server:
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "https://myapp.com");
next();
});
Or for multiple origins:
const allowedOrigins = ["https://myapp.com", "https://staging.myapp.com"];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
}
next();
});
Preflight returning 403 or 404
If the OPTIONS preflight route is not handled, the server returns an error instead of the expected headers.
Fix: Ensure your server handles OPTIONS requests for the relevant routes, even if there is no actual data to return:
app.options("/api/resource", (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "https://myapp.com");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.sendStatus(200);
});
In Next.js App Router, the default behavior handles OPTIONS automatically as long as the middleware sets the right headers for non-matching routes.
Credentials and wildcard
You cannot use Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. The browser rejects this combination.
Fix: Always specify the exact origin when credentials are needed:
res.header("Access-Control-Allow-Origin", "https://myapp.com"); // not *
res.header("Access-Control-Allow-Credentials", "true");
CORS on static sites and CDNs
When hosting on a static site with a CDN (Cloudflare Pages, Netlify, Vercel, S3+CloudFront), you configure CORS at the CDN or object storage layer, not in application code.
On Cloudflare Pages, CORS headers are set via a _headers file in the project root:
/api/*
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
On AWS S3, you set CORS configuration in the bucket properties under "CORS configuration", using JSON:
[
{
"AllowedOrigins": ["https://myapp.com"],
"AllowedMethods": ["GET", "POST"],
"AllowedHeaders": ["Content-Type"],
"MaxAgeSeconds": 86400
}
]
CORS vs JSON-P
Before CORS existed, developers worked around the same-origin policy using JSON with Padding (JSON-P): wrapping JSON data inside a callback function and loading it via a <script> tag. Because <script> tags are not subject to the same-origin policy, this allowed cross-origin data retrieval.
JSON-P has two significant problems: it requires the server to cooperate (it must wrap its response in a function call), and it is inherently insecure — it executes the returned JavaScript in your page context, so the server can inject arbitrary code.
CORS is the modern, correct solution. If you see JSON-P in use, it is a sign that the API predates CORS or was not designed with security in mind.
FAQ
Does CORS affect server-to-server requests?
No. CORS is a browser-enforced mechanism. If a server makes a request to another server using curl, a server-side HTTP client, or any non-browser tool, the same-origin policy does not apply and no CORS headers are checked. CORS only matters when JavaScript running inside a browser initiates the request.
Is Access-Control-Allow-Origin: * ever safe to use?
Yes, but only for public, non-sensitive resources that do not require credentials. For example, a CDN serving fonts, a public API with no authentication, or static assets. Any resource that uses cookies, session tokens, or any form of credentials must specify the exact allowed origin — * is not permitted with credentials.
Does CORS protect against attacks?
CORS does not prevent malicious cross-origin requests. A malicious site can still send a POST or GET to your API — CORS headers do not stop the request from being sent; they only control whether the browser lets JavaScript read the response. Protect your API with authentication, rate limiting, and CSRF tokens. CORS is a convenience mechanism for legitimate cross-origin access, not a security firewall.
Why does my browser send an OPTIONS request before every POST?
Your POST request likely has a Content-Type of application/json or includes a custom header. That makes it a non-simple request, triggering a preflight OPTIONS. This is normal behavior. The OPTIONS request is fast — it only carries headers, no body. If preflights are causing latency issues, you can reduce their frequency by setting Access-Control-Max-Age to a higher value (e.g., 3600 for one hour).
Verdict
CORS is a browser security mechanism that gives servers control over who can read their resources from a web page. It uses HTTP headers to let servers permit cross-origin access, and the browser enforces those decisions. Understanding CORS means understanding the same-origin policy, the difference between simple and preflighted requests, and which headers control what. When you encounter a CORS error in development, the fix is almost always on the server — add the right Access-Control-Allow-Origin header and handle OPTIONS for preflight requests. CORS is not a security wall for your API, but it is the correct and standard way to enable legitimate cross-origin resource sharing.
Sources
- MDN Web Docs — CORS (developer.mozilla.org/en-US/docs/Web/HTTP/CORS
- CC-BY-SA 2.5)
- web.dev — Cross-Origin Resource Sharing (web.dev/articles/cross-origin-resource-sharing
- Apache 2.0)
- IETF RFC 9110 — HTTP Semantics (ietf.org/rfc/rfc9110
- informational)
- W3C — CORS W3C Recommendation (w3.org/TR/cors)
Share this guide
Related guides
- GraphQL vs gRPC: which API technology should you choose?
GraphQL and gRPC are two modern ways to build APIs. GraphQL lets clients query exactly the fields they need over a single HTTP endpoint; gRPC calls typed remote methods over HTTP/2 with binary Protocol Buffers. Compare the trade-offs and learn when to choose each.
- REST vs gRPC: which API style should you choose?
REST and gRPC are two ways to build APIs. REST uses HTTP and JSON with universal browser support; gRPC uses HTTP/2 and Protocol Buffers for binary efficiency and code-generated stubs. Compare the trade-offs and learn when to choose each.
- What is an API? A plain-language guide to how APIs work
What an API is, how REST APIs work over HTTP, the methods you'll actually meet — GET, POST, PUT, PATCH, DELETE — and how to read a response.