What Is CORS and Why Does It Exist?
If you have ever built a web application where the frontend talks to a separate backend API, you have almost certainly seen a bright red error in your browser console that reads something like “Access to fetch at … has been blocked by CORS policy.”
CORS stands for Cross-Origin Resource Sharing. It is a security mechanism enforced by web browsers that restricts web pages from making HTTP requests to a domain (or port, or protocol) different from the one that served the page. The browser does this to protect users from malicious sites that could silently call APIs on their behalf.
In practical terms, if your frontend is running on http://localhost:3000 and your API lives at http://localhost:5000, the browser considers these two different origins. Unless the server at port 5000 explicitly tells the browser “I allow requests from port 3000,” the browser will block the response.
Understanding the mechanism behind CORS is the first step to fixing it correctly and securely. Let’s break it all down.
How CORS Works Under the Hood
When your JavaScript code in the browser makes a cross-origin request, the browser adds an Origin header to the outgoing request. The server is expected to respond with specific headers that tell the browser whether the request is allowed.
The most important response header is:
Access-Control-Allow-Origin– Specifies which origins are permitted to access the resource.
There are additional headers involved depending on the complexity of the request:
| Response Header | Purpose |
|---|---|
Access-Control-Allow-Origin |
Declares allowed origin(s). Can be a specific URL or * (wildcard). |
Access-Control-Allow-Methods |
Lists HTTP methods the server permits (GET, POST, PUT, DELETE, etc.). |
Access-Control-Allow-Headers |
Specifies which custom headers the client is allowed to send. |
Access-Control-Allow-Credentials |
Indicates whether cookies or auth headers can be included. |
Access-Control-Max-Age |
How long (in seconds) the preflight response can be cached. |
What Is a Preflight Request?
Not all cross-origin requests are treated equally. 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 HTTP method is
GET,HEAD, orPOST. - The only headers set manually are from the safe list:
Accept,Accept-Language,Content-Language, orContent-Type(with values limited toapplication/x-www-form-urlencoded,multipart/form-data, ortext/plain).
Simple requests go directly to the server. The browser checks the response headers and either exposes or blocks the response.
Preflighted Requests
If a request does not qualify as simple (for example, it uses PUT or DELETE, or sends a Content-Type: application/json header), the browser first sends an automatic OPTIONS request called a preflight. This preflight asks the server: “Are you okay with this type of request from this origin?”
Only if the server responds to the OPTIONS request with the correct CORS headers will the browser proceed to send the actual request.
This is where many developers get tripped up. Their server handles the main request fine but does not respond to the preflight OPTIONS request correctly, resulting in a CORS error.
Most Common CORS Error Scenarios
Here are the situations developers run into most frequently:
- No
Access-Control-Allow-Originheader in the response. The server simply does not include the header at all. - Wildcard origin with credentials. Using
Access-Control-Allow-Origin: *while also settingAccess-Control-Allow-Credentials: trueis not allowed by browsers. - Missing preflight handling. The server does not respond to
OPTIONSrequests, or returns a non-2xx status code for them. - Mismatched allowed methods or headers. The server allows
GETandPOSTbut the client sends aPUTrequest. - Protocol or port mismatch. The frontend is on
httpsbut calling anhttpAPI, or the ports differ. - Redirect on a preflight request. If the OPTIONS request gets redirected (e.g., HTTP to HTTPS), the browser will reject it.
How to Fix CORS Errors: Concrete Configurations
Let’s walk through the most popular server environments and show exactly how to configure CORS headers correctly.
Fix CORS in Node.js with Express
The easiest approach is to use the cors npm package.
Step 1: Install the package:
npm install cors
Step 2: Use it in your Express app:
const express = require('express');
const cors = require('cors');
const app = express();
// Allow a specific origin
app.use(cors({
origin: 'https://yourfrontend.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
}));
app.listen(5000, () => {
console.log('Server running on port 5000');
});
If you need to allow multiple origins dynamically:
const allowedOrigins = ['https://yourfrontend.com', 'https://staging.yourfrontend.com'];
app.use(cors({
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
Manual Approach (Without the cors Package)
If you prefer not to use a third-party package, you can set headers manually with middleware:
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://yourfrontend.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
// Handle preflight
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
Key point: Always handle the OPTIONS method explicitly and return a 204 No Content status for preflight requests.
Fix CORS in Apache
For Apache servers, you add CORS headers in your .htaccess file or in the virtual host configuration.
<IfModule mod_headers.c>
Header set Access-Control-Allow-Origin "https://yourfrontend.com"
Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type, Authorization"
Header set Access-Control-Allow-Credentials "true"
# Handle preflight requests
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
</IfModule>
Make sure mod_headers and mod_rewrite are enabled on your Apache server:
sudo a2enmod headers
sudo a2enmod rewrite
sudo systemctl restart apache2
Fix CORS in Nginx
In your Nginx server block or location block, add the following:
location /api/ {
# Preflight request handling
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://yourfrontend.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain';
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://yourfrontend.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
proxy_pass http://127.0.0.1:5000;
}
Important: The always keyword ensures headers are sent even on error responses (4xx, 5xx). Without it, Nginx may strip CORS headers from error responses, causing confusing intermittent failures.
Quick Reference: Server Configuration Comparison
| Server | Where to Configure | Preflight Handling | Ease of Setup |
|---|---|---|---|
| Node.js / Express | Middleware (cors package or manual) | Handled by middleware | Very easy |
| Apache | .htaccess or virtual host config | RewriteRule for OPTIONS | Moderate |
| Nginx | Server block / location block | if block returning 204 | Moderate |
Best Practices for Handling CORS Safely
Fixing CORS errors is one thing. Doing it securely is another. Follow these best practices to avoid opening your API to abuse:
- Never use
*as the allowed origin in production. A wildcard allows any website in the world to call your API from the browser. Always whitelist specific origins. - Be explicit with allowed methods. Only list the HTTP methods your API actually uses. If your API only needs GET and POST, do not allow PUT or DELETE.
- Limit allowed headers. Only permit the headers your application actually sends. A common set is
Content-TypeandAuthorization. - Set
Access-Control-Max-Ageto cache preflight responses. A value like86400(24 hours) reduces the number of OPTIONS requests, improving performance. - Only enable
credentials: truewhen you actually need cookies or auth headers. Enabling credentials mode imposes stricter rules and you cannot use a wildcard origin alongside it. - Validate the Origin header server-side. If you dynamically reflect the origin, make sure you check it against a whitelist. Otherwise, any origin could be reflected back, defeating the purpose.
- Use HTTPS in production. Mixed content (HTTPS page calling HTTP API) will be blocked by browsers independently of CORS.
Debugging CORS Errors: A Step-by-Step Approach
When you hit a CORS error, follow this checklist to diagnose the issue quickly:
- Open the browser DevTools (Network tab). Look at both the preflight OPTIONS request and the actual request. Check their status codes and response headers.
- Read the full error message in the Console tab. Modern browsers like Chrome and Firefox give detailed CORS error messages that tell you exactly which header is missing or misconfigured.
- Verify the
Originheader in the request. Make sure it matches what your server expects. - Check for redirects. If your server redirects the preflight (for example, HTTP to HTTPS, or adding a trailing slash), the browser will block it. Preflight requests must not be redirected.
- Test with a tool like
curl. You can simulate a preflight request to see exactly what your server returns:
curl -X OPTIONS https://yourapi.com/endpoint \
-H "Origin: https://yourfrontend.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type" \
-v
Look at the response headers. If Access-Control-Allow-Origin is missing or incorrect, you have found the problem.
Using a Proxy During Local Development
During local development, you may not want to configure CORS headers on your backend at all. A common approach is to use a development proxy so the browser thinks all requests go to the same origin.
Proxy with Vite
In your vite.config.js:
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
};
Proxy with Create React App (Webpack)
In your package.json:
"proxy": "http://localhost:5000"
With a proxy, your frontend requests go to /api/data on the same origin, and the dev server forwards them to the backend. No CORS headers needed during development.
Warning: A dev proxy is not a production solution. Always configure proper CORS headers on your production server.
Common Mistakes That Cause Subtle CORS Failures
Some CORS problems are not immediately obvious. Here are a few tricky ones to watch for:
- Trailing slashes in the origin.
https://example.comandhttps://example.com/are treated as different origins by some servers. Make sure you are consistent. - Double CORS headers. If both your reverse proxy (Nginx) and your application (Express) add CORS headers, the browser may receive duplicate headers and reject the response. Pick one layer to handle CORS.
- Forgetting about WebSocket connections. CORS does not apply to WebSocket upgrade requests, but the initial HTTP handshake might be affected. Check your configuration if you use WebSockets.
- Browser extensions interfering. Some browser extensions modify headers or disable CORS for development. This can mask issues that will appear in production. Always test without extensions.
- Caching stale preflight responses. If you change your CORS configuration and the browser has cached a previous preflight response, you may still see errors. Clear the cache or use an incognito window to test.
CORS and Different HTTP Status Codes
One thing that confuses developers is that CORS errors are not HTTP errors. A CORS failure does not produce a specific HTTP status code like 403 or 401. Instead, the browser receives (or does not receive) the correct headers and decides on its own to block the response.
However, if your server returns a 500 error on the OPTIONS preflight, or if it returns a 301 redirect, the browser will treat these as CORS failures because the preflight did not succeed with a 2xx status and the right headers.
The takeaway: make sure your OPTIONS handler returns a 200 or 204 status with the correct headers, regardless of what the rest of your application does.
Frequently Asked Questions
What does a CORS error actually mean?
A CORS error means the browser blocked a cross-origin HTTP request because the server’s response did not include the required CORS headers (like Access-Control-Allow-Origin). The request may have actually reached the server, but the browser refuses to let your JavaScript code access the response.
Can I fix CORS errors from the frontend?
No. CORS is enforced by the browser based on headers sent by the server. The frontend cannot override this behavior. The fix always involves configuring the server (or using a proxy during development).
Is using Access-Control-Allow-Origin: * safe?
It depends on the context. For a truly public API with no authentication, a wildcard can be acceptable. For any API that uses cookies, tokens, or handles sensitive data, you should always specify exact allowed origins instead of using a wildcard.
Why does my GET request work but my POST request fails with a CORS error?
A POST request with a Content-Type: application/json header triggers a preflight OPTIONS request. If your server does not handle the preflight correctly, the browser blocks the POST. A simple GET request often does not trigger a preflight, which is why it appears to work.
Do CORS errors happen with server-to-server requests?
No. CORS is purely a browser security feature. Server-to-server HTTP requests (for example, a Node.js backend calling another API) are not subject to CORS restrictions. This is why tools like curl or Postman never show CORS errors.
How do I allow multiple origins without using a wildcard?
The Access-Control-Allow-Origin header only accepts a single value. To support multiple origins, your server must read the incoming Origin header, check it against a whitelist, and dynamically set the response header to match the allowed origin. See the dynamic origin example in the Express section above.
What is the Access-Control-Max-Age header for?
It tells the browser how long (in seconds) to cache the preflight response. During that period, the browser will not send another OPTIONS request for the same endpoint, reducing overhead and improving performance. A typical value is 86400 seconds (24 hours).
Wrapping Up
CORS errors are one of the most common frustrations in web development, but they are not mysterious once you understand what the browser is actually checking. The fix is always on the server side: set the correct headers, handle preflight OPTIONS requests, and be intentional about which origins, methods, and headers you allow.
Whether you are working with Node.js and Express, configuring Apache, or tuning Nginx, the core principles are the same. Whitelist your origins, handle preflight properly, and never use a wildcard in production for authenticated APIs.
If you found this guide helpful, feel free to bookmark it for the next time a CORS error catches you off guard. At Pixelating Bits, we publish practical development guides to help you spend less time debugging and more time building.