You click a button on your web app. Nothing happens. You open the browser console and see a red error message about CORS. Your application can’t fetch data from your API. This frustrating scenario happens to developers every day, and it all comes down to one security mechanism: CORS, or Cross-Origin Resource Sharing.
What Is CORS and Why Does It Exist
CORS stands for Cross-Origin Resource Sharing. It’s a security feature built into web browsers that controls how web pages from one domain can request resources from another domain.
Here’s the basic problem CORS solves: Without restrictions, any website could make requests to your bank’s API using your logged-in session. A malicious site could steal your data or perform actions without your knowledge. This is called a cross-site request forgery attack.
Browsers prevent this by default. They implement the Same-Origin Policy, which blocks requests between different origins unless the server explicitly allows them.
An origin consists of three parts:
- Protocol (http or https)
- Domain (example.com)
- Port (80, 443, 8080, etc.)
If any of these differ between the requesting page and the resource, the browser considers it a cross-origin request.
Examples of same-origin vs cross-origin:
| Request From | Request To | Same Origin? |
|---|---|---|
| https://example.com | https://example.com/api | Yes |
| https://example.com | http://example.com/api | No (protocol differs) |
| https://example.com | https://api.example.com | No (subdomain differs) |
| https://example.com:443 | https://example.com:8080 | No (port differs) |
When your frontend application on https://myapp.com tries to fetch data from https://api.backend.com, the browser blocks it by default. The server at api.backend.com must send CORS headers to tell the browser it’s safe to allow this request.
What Is Access-Control-Allow-Origin
Access-Control-Allow-Origin is the most important CORS header. It tells browsers which origins have permission to read responses from the server.
When a browser makes a cross-origin request, it checks the response for this header. If the header is missing or doesn’t match the requesting origin, the browser blocks your JavaScript from accessing the response data.
The header has three possible values:
Access-Control-Allow-Origin: *This allows any origin to access the resource. It’s simple but often insecure for APIs that handle user data or authentication.
Access-Control-Allow-Origin: https://myapp.comThis allows only the specific origin listed. This is more secure and appropriate for most applications.
Access-Control-Allow-Origin: null
This is rarely useful and can create security issues. Avoid using it.
The server must send this header in the HTTP response. You cannot set it from JavaScript in the browser. Client-side code cannot bypass CORS restrictions.

How CORS Requests Work
Understanding how browsers handle CORS requests will help you debug issues and implement solutions correctly.
Simple Requests
Some requests are “simple” and the browser sends them directly. For a request to be simple, it must meet all these conditions:
- Method is GET, HEAD, or POST
- Only contains safe headers (Accept, Accept-Language, Content-Language, Content-Type)
- Content-Type (if present) is application/x-www-form-urlencoded, multipart/form-data, or text/plain
For simple requests:
- Browser sends the request with an Origin header
- Server processes the request
- Server sends response with Access-Control-Allow-Origin header
- Browser checks if the origin matches
- If it matches, JavaScript can access the response
- If not, browser blocks access and throws an error
Preflight Requests
Most modern API requests are not simple. If your request uses PUT, DELETE, PATCH, custom headers, or Content-Type: application/json, the browser sends a preflight request first.
The preflight is an OPTIONS request that asks the server what’s allowed:
- Browser sends OPTIONS request to same URL
- OPTIONS request includes Origin, Access-Control-Request-Method, and Access-Control-Request-Headers
- Server responds with multiple Access-Control headers
- Browser checks if the actual request is permitted
- If yes, browser sends the actual request
- If no, browser blocks the request before it’s sent
This two-step process protects servers from receiving unexpected requests.
How to Allow CORS Access-Control-Allow-Origin
The implementation depends on your server technology. Here are practical examples for common platforms.
Node.js with Express
The simplest approach uses the cors package:
const express = require('express');
const cors = require('cors');
const app = express();
// Allow all origins (development only)
app.use(cors());
// Allow specific origin (production)
app.use(cors({
origin: 'https://myapp.com'
}));
// Allow multiple origins
const allowedOrigins = ['https://myapp.com', 'https://admin.myapp.com'];
app.use(cors({
origin: function(origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}));Without the package, you can set headers manually:
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://myapp.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});Python with Flask
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
# Allow all origins (development only)
CORS(app)
# Allow specific origin
CORS(app, resources={r"/api/*": {"origins": "https://myapp.com"}})
# Manual configuration
@app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Origin', 'https://myapp.com')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE')
return responsePHP
<?php
header('Access-Control-Allow-Origin: https://myapp.com');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
?>Apache (.htaccess)
<IfModule mod_headers.c>
Header set Access-Control-Allow-Origin "https://myapp.com"
Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type, Authorization"
</IfModule>Nginx
location /api {
add_header Access-Control-Allow-Origin "https://myapp.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
if ($request_method = OPTIONS) {
return 204;
}
}Common CORS Headers Explained
Access-Control-Allow-Origin is just one of several CORS headers. Understanding all of them helps you configure CORS properly.
Access-Control-Allow-Methods
Specifies which HTTP methods are allowed for cross-origin requests.
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Include all methods your API supports. OPTIONS must be included to handle preflight requests.
Access-Control-Allow-Headers
Lists which HTTP headers can be used in the actual request.
Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header
Common headers include Content-Type for JSON requests and Authorization for bearer tokens. Add any custom headers your application uses.
Access-Control-Allow-Credentials
Indicates whether the browser should include credentials (cookies, authorization headers) in cross-origin requests.
Access-Control-Allow-Credentials: trueWhen set to true, you cannot use Access-Control-Allow-Origin: *. You must specify an exact origin.
Client-side setup:
fetch('https://api.backend.com/data', {
credentials: 'include'
});Access-Control-Max-Age
Tells the browser how long to cache preflight results.
Access-Control-Max-Age: 86400The value is in seconds. This reduces the number of preflight requests for the same endpoint.
Access-Control-Expose-Headers
By default, browsers only expose safe headers to JavaScript. If your API sends custom headers that the client needs to read, list them here:
Access-Control-Expose-Headers: X-Total-Count, X-Page-NumberSecurity Best Practices for CORS Configuration
CORS configuration directly impacts your application’s security. Follow these guidelines to avoid common vulnerabilities.
Never Use Wildcard in Production with Credentials
This configuration is dangerous:
// DANGEROUS - DO NOT USE
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Credentials', 'true');Browsers will reject this combination. But even attempting it shows a misunderstanding of CORS security.
Validate Origins Properly
Don’t use simple string matching for origin validation. This is vulnerable:
// VULNERABLE
const origin = req.headers.origin;
if (origin && origin.includes('myapp.com')) {
res.header('Access-Control-Allow-Origin', origin);
}An attacker could use evil-myapp.com or myapp.com.attacker.com and pass this check.
Better approach:
const allowedOrigins = [
'https://myapp.com',
'https://admin.myapp.com'
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}Use HTTPS for Production Origins
Always use HTTPS in your allowed origins list. HTTP connections are vulnerable to man-in-the-middle attacks.
// Good
origin: 'https://myapp.com'
// Bad for production
origin: 'http://myapp.com'Limit Allowed Methods and Headers
Only allow the HTTP methods and headers your API actually uses. Don’t copy-paste broad configurations.
// Too permissive
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD, CONNECT, TRACE
// Better
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONSBe Careful with User Data Endpoints
For endpoints that return sensitive user data, be extra strict with origin validation. Consider implementing additional authentication checks beyond CORS.
CORS prevents browsers from reading responses, but it doesn’t prevent requests from being sent. A malicious site can still trigger state-changing requests if you only rely on CORS for protection.
Common CORS Errors and How to Fix Them
Error: No Access-Control-Allow-Origin Header Present
Full error message:
Access to fetch at 'https://api.backend.com/data' from origin 'https://myapp.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is
present on the requested resource.Cause: The server isn’t sending the CORS header at all.
Fix:
- Verify your server code is actually running the CORS middleware
- Check that the endpoint exists and isn’t returning 404
- Look at actual response headers in browser DevTools Network tab
- Make sure CORS headers are sent even for error responses
Error: Origin Not Allowed
Full error message:
Access to fetch at 'https://api.backend.com/data' from origin 'https://myapp.com'
has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a
value 'https://other-app.com' that is not equal to the supplied origin.Cause: The server is sending a CORS header, but it doesn’t match your origin.
Fix:
- Add your origin to the allowed list
- Check for typos in the origin URL (http vs https, www vs non-www)
- Verify the origin matches exactly, including port numbers
- Make sure you’re not sending
Access-Control-Allow-Origin: *with credentials
Error: Preflight Request Failed
Full error message:
Access to fetch at 'https://api.backend.com/data' from origin 'https://myapp.com'
has been blocked by CORS policy: Response to preflight request doesn't pass
access control check: It does not have HTTP ok status.Cause: The OPTIONS preflight request failed or returned an error status.
Fix:
- Make sure your server handles OPTIONS requests
- Return 200 or 204 status for OPTIONS requests
- Send all required CORS headers in the OPTIONS response
- Check that authentication middleware doesn’t block OPTIONS requests
- Verify the Access-Control-Allow-Methods header includes your request method
- Confirm Access-Control-Allow-Headers includes all your custom headers
Error: Credentials Flag Not Supported with Wildcard
Full error message:
Access to fetch at 'https://api.backend.com/data' from origin 'https://myapp.com'
has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin'
header in the response must not be the wildcard '*' when the request's
credentials mode is 'include'.Cause: You’re trying to use cookies or authorization with Access-Control-Allow-Origin: *.
Fix:
Replace the wildcard with the specific origin:
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Credentials', 'true');Make sure you validate the origin before reflecting it.
Testing CORS Configuration
Proper testing ensures your CORS setup works correctly across different scenarios.
Using Browser DevTools
- Open your web application
- Open DevTools (F12)
- Go to Network tab
- Trigger the cross-origin request
- Click on the request in the Network tab
- Look at the Headers section
Check these items:
- Request Headers: Verify Origin header is present
- Response Headers: Confirm Access-Control-Allow-Origin matches your origin
- Status: Should be 200 for the actual request, 200 or 204 for OPTIONS
- Console: Check for any CORS error messages
Using curl for Preflight Testing
Test a preflight request manually:
curl -X OPTIONS https://api.backend.com/data \
-H "Origin: https://myapp.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
-iLook for these headers in the response:
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, AuthorizationUsing Online CORS Testing Tools
Several online tools can test your CORS configuration without writing code:
- test-cors.org provides a simple interface to send cross-origin requests
- Postman can simulate CORS requests by disabling its own security features
Remember that Postman and similar API clients don’t enforce CORS by default. They can fetch resources that browsers would block. Always test in an actual browser for accurate results.
Testing Different Scenarios
Create a test checklist:
- Simple GET request without credentials
- POST request with JSON body
- Request with Authorization header
- Request with credentials (cookies)
- Request from a disallowed origin (should fail)
- Request with unsupported method (should fail)
- Request with custom headers
CORS Alternatives and When to Use Them
CORS isn’t always the best solution. Here are alternative approaches for different situations.
Proxy Server
Instead of enabling CORS, route API requests through your own server.
How it works:
- Frontend makes request to your domain
- Your server forwards the request to the third-party API
- Your server sends the response back to the frontend
- No CORS issues because everything is same-origin
When to use:
- You can’t modify the API server to add CORS headers
- You need to hide API keys from the client
- You want to add rate limiting or caching
- The third-party API doesn’t support CORS
Example with Node.js:
app.get('/api/proxy', async (req, res) => {
const response = await fetch('https://third-party-api.com/data', {
headers: {
'Authorization': `Bearer ${process.env.API_KEY}`
}
});
const data = await response.json();
res.json(data);
});JSONP (Legacy)
JSONP uses script tags to bypass same-origin policy. This technique is outdated and has security issues, but you might encounter it in older codebases.
Don’t use JSONP for new projects. Use CORS or a proxy instead.
Server-Side Rendering
Render your application on the server and fetch data there. The browser only receives the final HTML.
When to use:
- You need SEO for content that requires API data
- You want to improve initial page load performance
- You can run server-side JavaScript or your preferred backend language
Frameworks like Next.js, Nuxt.js, and SvelteKit make this approach easier.
WebSocket Connections
For real-time data, WebSocket connections have different origin policies than HTTP requests.
WebSockets check the Origin header but don’t use CORS. The server must validate the origin manually during the handshake.
Development vs Production CORS Configuration
Your CORS setup should differ between development and production environments.
Development Configuration
During development, you might allow all origins for convenience:
if (process.env.NODE_ENV === 'development') {
app.use(cors());
} else {
app.use(cors({
origin: allowedOrigins
}));
}This works when your frontend runs on localhost:3000 and your backend runs on localhost:5000.
Better development approach:
const allowedOrigins = process.env.NODE_ENV === 'development'
? ['http://localhost:3000', 'http://localhost:3001']
: ['https://myapp.com', 'https://admin.myapp.com'];This is more secure and closer to production behavior.
Production Configuration
In production:
- Use specific origins, never wildcards with credentials
- Use HTTPS for all origins
- Set appropriate cache headers with Access-Control-Max-Age
- Log CORS errors to catch configuration issues
- Consider using environment variables for origin lists
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
app.use(cors({
origin: function(origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
console.error('CORS blocked origin:', origin);
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
maxAge: 86400
}));Environment Variables
Store origins in environment variables:
ALLOWED_ORIGINS=https://myapp.com,https://admin.myapp.comThis keeps configuration separate from code and makes it easy to update without redeployment.
Advanced CORS Patterns
Dynamic Origin Validation
For SaaS applications where customers have custom domains, you might need database-driven origin validation:
async function isOriginAllowed(origin) {
const customer = await db.customers.findOne({
allowedOrigins: origin
});
return !!customer;
}
app.use(cors({
origin: async function(origin, callback) {
const allowed = await isOriginAllowed(origin);
callback(null, allowed);
}
}));Subdomain Wildcarding
Allow all subdomains of your domain:
function isSubdomain(origin, baseDomain) {
try {
const url = new URL(origin);
return url.hostname === baseDomain ||
url.hostname.endsWith(`.${baseDomain}`);
} catch {
return false;
}
}
app.use(cors({
origin: function(origin, callback) {
if (!origin || isSubdomain(origin, 'myapp.com')) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}));Conditional CORS by Endpoint
Some endpoints might need different CORS policies:
// Public API - allow all origins
app.get('/api/public/data', cors(), (req, res) => {
res.json({ data: 'public' });
});
// Private API - restrict origins
const privateCors = cors({
origin: 'https://myapp.com',
credentials: true
});
app.get('/api/private/user', privateCors, (req, res) => {
res.json({ user: 'private' });
});Monitoring and Debugging CORS Issues
Server-Side Logging
Add logging to track CORS requests and failures:
app.use((req, res, next) => {
const origin = req.headers.origin;
console.log({
method: req.method,
path: req.path,
origin: origin,
timestamp: new Date().toISOString()
});
next();
});Client-Side Error Handling
Catch and handle CORS errors gracefully:
fetch('https://api.backend.com/data')
.then(response => response.json())
.catch(error => {
if (error.message.includes('CORS')) {
console.error('CORS error detected');
// Show user-friendly message
// Log to error tracking service
}
});Common Debugging Checklist
When CORS fails:
- Verify the request is actually cross-origin
- Check if the endpoint exists (not 404)
- Confirm CORS middleware is running
- Look at actual response headers in Network tab
- Test the OPTIONS request separately
- Verify origin string matches exactly
- Check if authentication blocks OPTIONS requests
- Test without credentials first
- Review server logs for errors
- Try a simple curl command to isolate browser issues
Understanding the Full CORS Request Flow
Let’s trace a complete CORS request from start to finish.
Scenario: Your React app at https://myapp.com sends a POST request with JSON to https://api.backend.com/users.
Step 1: Browser checks if preflight is needed
The request uses POST with Content-Type: application/json, so it’s not simple. Browser decides to send a preflight.
Step 2: Browser sends OPTIONS request
OPTIONS /users HTTP/1.1
Host: api.backend.com
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-typeStep 3: Server handles OPTIONS
Your server processes the OPTIONS request and sends headers:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400Step 4: Browser validates preflight response
Browser checks:
- Is the origin allowed? Yes, it matches.
- Is POST allowed? Yes, it’s in Allow-Methods.
- Is Content-Type header allowed? Yes, it’s in Allow-Headers.
All checks pass.
Step 5: Browser sends actual request
POST /users HTTP/1.1
Host: api.backend.com
Origin: https://myapp.com
Content-Type: application/json
{"name": "John", "email": "john@example.com"}Step 6: Server processes and responds
HTTP/1.1 201 Created
Access-Control-Allow-Origin: https://myapp.com
Content-Type: application/json
{"id": 123, "name": "John", "email": "john@example.com"}Step 7: Browser validates actual response
Browser checks the Access-Control-Allow-Origin header. It matches, so JavaScript can access the response.
Step 8: Your code receives the data
const response = await fetch('https://api.backend.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'John',
email: 'john@example.com'
})
});
const user = await response.json();
console.log(user); // {id: 123, name: "John", email: "john@example.com"}This entire process happens automatically. You only see the final result or error message.
CORS and Modern API Standards
Modern API design incorporates CORS from the start rather than adding it as an afterthought.
REST APIs
RESTful APIs commonly serve multiple client applications. Configure CORS to allow your known frontends while blocking others.
Best practice: Maintain a list of approved origins in your configuration rather than hardcoding them.
GraphQL APIs
GraphQL endpoints need the same CORS configuration as REST APIs. Since GraphQL typically uses a single POST endpoint, make sure your CORS setup allows POST with Content-Type: application/json.
app.use('/graphql', cors({
origin: allowedOrigins,
credentials: true
}), graphqlHTTP({
schema: schema,
graphiql: true
}));Microservices
In a microservices architecture, you might have multiple APIs. Each service needs its own CORS configuration.
API Gateway approach: Configure CORS once at the gateway level rather than in each service. This centralizes security policy.
// API Gateway
app.use(cors({
origin: allowedOrigins,
credentials: true
}));
app.use('/users', proxy('http://user-service:3001'));
app.use('/orders', proxy('http://order-service:3002'));
app.use('/products', proxy('http://product-service:3003'));Summary
CORS (Cross-Origin Resource Sharing) is a browser security feature that controls which websites can access resources from your API. When you see CORS errors, it means the server hasn’t explicitly allowed your frontend’s origin.
The Access-Control-Allow-Origin header is the key to fixing CORS issues. Set it to your frontend’s exact origin (like https://myapp.com) in your server’s response headers. Never use the wildcard * when your application uses credentials like cookies or authorization tokens.
Most CORS errors fall into a few categories: missing headers, mismatched origins, or failed preflight requests. Check your browser’s Network tab to see the actual headers being sent and received. Make sure your server handles OPTIONS requests properly and sends all necessary CORS headers.
For development, you can allow localhost origins or use a CORS middleware with permissive settings. For production, always specify exact origins, use HTTPS, and validate origins properly to prevent security vulnerabilities.
Frequently Asked Questions
Why does my API work in Postman but not in the browser?
Postman doesn’t enforce CORS policies like browsers do. Postman is a development tool that ignores same-origin policy. When you test in a browser, the browser’s security features block cross-origin requests without proper CORS headers. Always test your CORS configuration in an actual browser to see real behavior. You can also use browser DevTools to check the Network tab for CORS-related headers and errors.
Can I disable CORS completely?
You cannot disable CORS from the client side. CORS is a browser security feature that you cannot turn off through JavaScript. You can only configure it on the server by sending appropriate headers. During development, you might use browser extensions that disable CORS checks, but this only affects your local browser and doesn’t solve the problem for your users. The proper solution is to configure CORS headers on your server.
What’s the difference between CORS and CSRF?
CORS and CSRF (Cross-Site Request Forgery) are different security mechanisms. CORS controls whether browsers allow JavaScript to read responses from cross-origin requests. CSRF protection prevents malicious sites from triggering state-changing requests using your credentials. You need both protections. CORS doesn’t prevent requests from being sent, it only blocks reading responses. Use CSRF tokens for actions that change data, even when CORS is properly configured.
Do I need CORS for same-origin requests?
No, CORS only applies to cross-origin requests. If your frontend and API are on the same origin (same protocol, domain, and port), the browser allows the request without any CORS headers. For example, if both run on https://myapp.com, no CORS configuration is needed. This is why many applications serve their frontend and API from the same domain or use subpaths like https://myapp.com/app and https://myapp.com/api.
Should I allow credentials with Access-Control-Allow-Credentials?
Only enable credentials if your application needs to send cookies or authorization headers with cross-origin requests. When you set Access-Control-Allow-Credentials: true, you must specify an exact origin in Access-Control-Allow-Origin, never a wildcard. You also need to set credentials: 'include' in your fetch requests. If you’re using bearer tokens in the Authorization header and don’t need cookies, you might not need this header at all since the Authorization header can be sent without the credentials flag.
