CORS Explained: Cross-Origin Resource Sharing for Developers
Understand how CORS works, why browsers enforce the same-origin policy, how preflight requests work, and how to fix common CORS errors in your applications.
CORS (Cross-Origin Resource Sharing) is the mechanism browsers use to let a server explicitly allow JavaScript on other origins to read its responses — the server sends HTTP headers specifying which origins, methods, and headers are permitted, and the browser enforces that policy. You have probably seen it fail: a red message in your browser console reading something like “Access to fetch at ‘https://api.example.com’ from origin ‘http://localhost:3000’ has been blocked by CORS policy,” even though the same request works fine in Postman.
CORS is not your enemy. It is a security mechanism that protects your users. Once you understand how it works, fixing CORS issues becomes straightforward. This guide explains the underlying concepts, the HTTP headers involved, and how to resolve the most common problems.
The Same-Origin Policy
Before CORS existed, browsers enforced a strict rule: JavaScript on a web page could only make requests to the same origin that served the page. An origin is the combination of protocol, domain, and port.
| URL | Origin |
|---|---|
https://example.com/page |
https://example.com |
https://example.com:8080/api |
https://example.com:8080 |
http://example.com/page |
http://example.com |
https://api.example.com/data |
https://api.example.com |
All four of these are different origins. Even changing the port or switching from http to https creates a new origin.
The same-origin policy exists because without it, a malicious website could use your authenticated session to make requests to your bank, email, or any other service where you are logged in. The browser prevents this by blocking cross-origin requests from JavaScript by default.
What CORS Actually Does
CORS (Cross-Origin Resource Sharing) is a way for a server to tell the browser: “I am okay with requests from this other origin.” It uses HTTP headers to communicate which origins, methods, and headers are allowed.
The key insight is that CORS is enforced by the browser, not the server. The server sets headers that express its policy, and the browser decides whether to allow the JavaScript code to access the response. This is why requests work in Postman or curl — those tools do not enforce CORS.
Simple Requests vs Preflight Requests
Not all cross-origin requests are handled the same way. The browser categorizes them into simple requests and preflighted requests.
Simple Requests
A request is considered “simple” if it meets all of these conditions:
- The method is GET, HEAD, or POST
- The only headers set are Accept, Accept-Language, Content-Language, or Content-Type
- The Content-Type is
application/x-www-form-urlencoded,multipart/form-data, ortext/plain
For simple requests, the browser sends the request directly and checks the response headers to decide whether to expose the response to JavaScript.
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://mysite.com
The server responds:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://mysite.com
Content-Type: application/json
{"status": "ok"}
The browser sees that Access-Control-Allow-Origin matches the requesting origin and allows the JavaScript to read the response.
Preflight Requests
If your request does not qualify as “simple” — for example, you are sending JSON with Content-Type: application/json, or using PUT/DELETE methods, or sending custom headers — the browser sends a preflight request first.
A preflight is an OPTIONS request that asks the server what is allowed:
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://mysite.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
The server responds with what it permits:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://mysite.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Only if the preflight response indicates the actual request is allowed will the browser proceed to send it. The Access-Control-Max-Age header tells the browser how long to cache this preflight response, so it does not need to repeat the OPTIONS request for a while.
The CORS Headers
Here is a complete reference of the CORS-related headers and what they do.
Response Headers (Server to Browser)
| Header | Purpose |
|---|---|
Access-Control-Allow-Origin |
Which origin(s) can access the resource. Can be a specific origin or * |
Access-Control-Allow-Methods |
Allowed HTTP methods for the actual request |
Access-Control-Allow-Headers |
Allowed request headers for the actual request |
Access-Control-Expose-Headers |
Which response headers JavaScript can read (beyond the basic set) |
Access-Control-Allow-Credentials |
Whether cookies and auth headers can be included (true or omitted) |
Access-Control-Max-Age |
How long (in seconds) the preflight response can be cached |
Request Headers (Browser to Server)
| Header | Purpose |
|---|---|
Origin |
The origin of the requesting page (sent automatically by the browser) |
Access-Control-Request-Method |
The method the actual request will use (sent in preflight) |
Access-Control-Request-Headers |
The headers the actual request will include (sent in preflight) |
The Wildcard Trap
Setting Access-Control-Allow-Origin: * allows any origin to access your resource. This works for public APIs and CDN resources, but there is a catch: you cannot use the wildcard with credentials.
If your request includes cookies or an Authorization header (using credentials: 'include' in fetch), the server must respond with the specific origin, not *:
// This will NOT work
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
// This WILL work
Access-Control-Allow-Origin: https://mysite.com
Access-Control-Allow-Credentials: true
This means your server needs to read the Origin header from the request and reflect it back in the response (after validating it against an allowlist).
Common CORS Errors and Fixes
“No ‘Access-Control-Allow-Origin’ header is present”
The server is not sending CORS headers at all. You need to configure your server to include them. Our CORS header generator can produce the correct headers for your framework or reverse proxy.
Express.js fix:
const cors = require('cors');
app.use(cors({
origin: 'https://mysite.com',
credentials: true
}));
Nginx fix:
location /api/ {
add_header Access-Control-Allow-Origin "https://mysite.com";
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type, Authorization";
if ($request_method = OPTIONS) {
return 204;
}
}
“Method PUT is not allowed”
Your preflight response does not include the method you are trying to use. Add it to Access-Control-Allow-Methods.
“Request header field Authorization is not allowed”
The Authorization header is not listed in Access-Control-Allow-Headers. Add it to the preflight response.
“The value of ‘Access-Control-Allow-Credentials’ header must be ‘true’”
You are sending credentials but the server is not explicitly allowing them. Add Access-Control-Allow-Credentials: true and make sure you are not using the * wildcard for the origin.
CORS error only in production
Your local development server has CORS configured, but the production server or CDN does not. Check your deployment configuration, reverse proxy settings, and CDN headers.
Testing CORS Configuration
Before deploying, you should verify that your CORS headers are set correctly. You can use our CORS tester to check any URL and see exactly which headers are returned, what origins are allowed, and whether credentials are supported.
You can also test manually with curl:
# Test a simple request
curl -H "Origin: https://mysite.com" -I https://api.example.com/data
# Test a preflight request
curl -X OPTIONS \
-H "Origin: https://mysite.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
-I https://api.example.com/data
Look for the Access-Control-* headers in the response.
CORS and Security Considerations
CORS is not a security feature in the sense that it protects your API. It protects the user’s browser from making unauthorized cross-origin requests with their credentials. Here are some important points:
- CORS does not replace authentication. A properly configured CORS policy prevents browser-based attacks, but your API still needs authentication and authorization.
- Do not reflect all origins. If your server blindly reflects the
Originheader without checking it against an allowlist, you have effectively disabled CORS protection. - Be specific with allowed headers. Only allow the headers your API actually uses. Allowing arbitrary headers increases the attack surface.
- Use short max-age values during development. Long cache times for preflight responses can mask configuration changes and make debugging harder.
Proxy Approach for Development
During development, the simplest way to avoid CORS issues is to use a proxy. Most frontend build tools support this:
Vite:
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
}
}
}
}
Next.js:
// next.config.js
module.exports = {
async rewrites() {
return [
{ source: '/api/:path*', destination: 'https://api.example.com/:path*' }
];
}
};
This makes the API request appear to come from the same origin, so CORS is not triggered. Just remember that this is a development convenience — in production, you need proper CORS configuration on your server.
Summary
CORS is the browser’s way of enforcing controlled cross-origin access. The same-origin policy provides the default security, and CORS headers let servers opt in to sharing resources with specific origins. Preflight requests verify that non-simple requests are allowed before sending them.
When you hit a CORS error, the fix is always on the server side — configure the right headers for your use case. Use our CORS tester to verify your configuration, and remember that CORS protects users, not APIs. Your server still needs its own authentication and authorization layer.
Comments