If you've ever opened your browser's console and seen something like "Access to fetch at 'https://api.example.com' from origin 'https://yoursite.com' has been blocked by CORS policy," you've hit a CORS error. Knowing how to fix CORS errors starts with understanding why browsers block these requests in the first place, and then knowing exactly which server-side headers and configurations tell the browser it's safe to proceed.
Content Table
The Browser Same-Origin Policy
The browser same-origin policy is a security rule built into every modern browser. It says: JavaScript running on one origin cannot read the response from a request made to a different origin, unless that other origin explicitly permits it.
An "origin" is the combination of three things:
- Protocol (http vs. https)
- Domain (example.com vs. api.example.com)
- Port (3000 vs. 8080)
If any one of those three differs between your page's URL and the URL you're fetching from, you're making a cross-origin request, and the same-origin policy kicks in. So
https://app.example.com
and
https://api.example.com
are different origins, even though they share the same root domain.
What Is CORS and Why Does It Exist
CORS stands for Cross-Origin Resource Sharing . It's a mechanism, defined by the WHATWG Fetch specification , that lets servers opt in to allowing cross-origin requests from specific origins. Without CORS, the same-origin policy would block every cross-origin API call, which would make modern web apps essentially impossible to build.
CORS works entirely through HTTP headers. The server includes special response headers that tell the browser: "Yes, I trust this origin. You can let the script read this response." The browser enforces those permissions. Your JavaScript code never sees them directly.
What Triggers a CORS Error
A CORS error appears in your console when the browser sends a cross-origin request and the server either doesn't send the right headers or sends headers that don't match your origin. Here are the most common triggers:
-
Your frontend is on
http://localhost:3000and your API is onhttp://localhost:8000(different ports = different origins). - You're calling a third-party API that hasn't enabled CORS for your domain.
- Your server returns an error (like a 500) before the CORS headers are added.
-
You're sending a request with a custom header (like
Authorization) and the server doesn't explicitly allow it. -
Your server returns
Access-Control-Allow-Origin: *but the request includes credentials (cookies or HTTP auth), which requires a specific origin, not a wildcard.
How CORS Preflight Requests Work
Not every cross-origin request triggers a preflight. Browsers split cross-origin requests into two categories: "simple" requests and requests that need a preflight check.
Simple requests go straight through if they meet all of these conditions:
- Method is GET, POST, or HEAD.
-
Headers are limited to
Accept,Accept-Language,Content-Language, andContent-Type(with values ofapplication/x-www-form-urlencoded,multipart/form-data, ortext/plain). -
No custom headers like
Authorization.
Everything else triggers a
CORS preflight request
. This is an automatic
OPTIONS
request the browser sends before your actual request. It asks the server: "I'm about to send a DELETE request from origin https://app.example.com with an Authorization header. Is that okay?"
A preflight request looks like this:
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type
The server must respond with the appropriate CORS headers and a 200 or 204 status. If it does, the browser sends the actual request. If it doesn't, the browser blocks the request and you see a CORS error in the console, even though your actual request was never sent.
You can see preflight requests in the Network tab of DevTools. Look for
OPTIONS
requests to your API endpoint. If they're returning 404 or 405, your server isn't handling the OPTIONS method at that route.
The CORS Headers That Actually Fix the Problem
These are the HTTP response headers your server needs to send. Each one controls a specific aspect of what the browser allows.
| Header | What It Controls | Example Value |
|---|---|---|
Access-Control-Allow-Origin
|
Which origins can read the response |
https://app.example.com
or
*
|
Access-Control-Allow-Methods
|
Which HTTP methods are allowed |
GET, POST, PUT, DELETE, OPTIONS
|
Access-Control-Allow-Headers
|
Which request headers are allowed |
Authorization, Content-Type
|
Access-Control-Allow-Credentials
|
Whether cookies/auth can be included |
true
|
Access-Control-Max-Age
|
How long the browser caches preflight results (seconds) |
86400
|
Access-Control-Expose-Headers
|
Which response headers JavaScript can access |
X-Custom-Header, X-Request-Id
|
The most important one is
Access-Control-Allow-Origin
. Without it, the browser blocks every cross-origin read. The value must exactly match the requesting origin, or be
*
for public APIs that don't use credentials.
Access-Control-Allow-Origin: *
together with
Access-Control-Allow-Credentials: true
. The browser will reject this combination. When you need credentials, you must specify the exact origin.
CORS Configuration on Common Backends
Express (Node.js)
The easiest way is the
cors
npm package. Install it with
npm install cors
, then use it as middleware:
const express = require('express');
const cors = require('cors');
const app = express();
// Allow a specific origin
const corsOptions = {
origin: 'https://app.example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
maxAge: 86400
};
app.use(cors(corsOptions));
// Handle preflight for all routes
app.options('*', cors(corsOptions));
app.listen(3000);
If you need to allow multiple origins dynamically, pass a function to the
origin
option:
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
const corsOptions = {
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
};
app.use(cors(corsOptions));
Nginx
Add CORS headers directly in your Nginx server block. This approach handles preflight requests too:
server {
listen 80;
server_name api.example.com;
location / {
# Handle preflight requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
return 204;
}
# Add CORS headers to all other responses
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Credentials' 'true';
proxy_pass http://localhost:8080;
}
}
add_header
directive only applies to 2xx and 3xx responses by default. If your backend returns a 4xx or 5xx, the CORS headers won't be added. Use
add_header ... always;
at the end of each directive to fix this.
Apache
Enable
mod_headers
first (
sudo a2enmod headers
on Debian/Ubuntu), then add this to your
.htaccess
or virtual host config:
Header always set Access-Control-Allow-Origin "https://app.example.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type"
Header always set Access-Control-Allow-Credentials "true"
# Handle OPTIONS preflight
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
Using a Proxy During Development
If you're using Create React App or Vite and just need to bypass CORS locally, you can proxy API requests through your dev server instead of dealing with CORS at all. In Create React App, add this to
package.json
:
"proxy": "http://localhost:8000"
In Vite's
vite.config.js
:
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
}
This only works in development. In production, you still need proper CORS headers or a reverse proxy. This is also related to how API rate limiting interacts with cross-origin requests, since rate limits are usually enforced at the server or proxy layer alongside CORS.
Common CORS Mistakes to Avoid
- Setting CORS headers in the frontend. CORS headers go on the server response. Setting them in your JavaScript fetch call does nothing.
- Forgetting to handle OPTIONS. If your server returns 404 or 405 for OPTIONS requests, preflight fails and your actual request never goes through.
-
Using a wildcard with credentials.
Access-Control-Allow-Origin: *andAccess-Control-Allow-Credentials: truetogether always fail. Use a specific origin. - Missing CORS headers on error responses. If your server throws a 500 before the CORS middleware runs, the browser sees a response with no CORS headers and blocks it. Make sure CORS middleware runs before your route handlers and error handlers.
-
Trailing slashes in the origin.
https://app.example.com/is not the same ashttps://app.example.com. Browsers send the origin without a trailing slash. - Not reflecting the request origin dynamically. If you hard-code a single origin but need to support multiple, requests from unlisted origins will fail silently.
If you're building or debugging APIs and want to understand how the request-response cycle works at a deeper level, it's also worth reading about scheduled server-side tasks , since many API backends use scheduled jobs that also need proper origin handling when making outbound requests.
Understanding how your JSON payloads are structured can also save debugging time. A well-formatted response body is much easier to inspect when tracing CORS issues. Our JSON beautifier can help you quickly read and verify API response bodies when you're stepping through network requests in DevTools.
Inspect API responses faster while debugging CORS errors
When you're tracing cross-origin requests in the browser's Network tab, raw JSON responses can be hard to read. Use our free JSON beautifier to instantly format and inspect API response bodies so you can spot missing CORS headers or malformed payloads in seconds.
Try the JSON Beautifier →
CORS is enforced entirely by the browser, not by the server or the HTTP protocol itself. Tools like Postman and curl don't implement the same-origin policy, so they send and receive cross-origin requests without any restrictions. If your request works in Postman but fails in the browser, the problem is definitely missing or incorrect CORS headers on your server's response.
Browser extensions that disable CORS (like "Allow CORS: Access-Control-Allow-Origin" for Chrome) can unblock requests in your own browser during development, but they are not a real fix. They only affect your browser, not your users' browsers. Any production fix must come from the server. Extensions are useful for quick local testing, but never ship code that depends on them.
A simple request goes directly to the server without a prior check. It must use GET, POST, or HEAD, and cannot include custom headers or non-standard Content-Type values. A preflight request is an automatic OPTIONS request the browser sends first when your request doesn't meet those conditions. The server must respond to the OPTIONS request with the correct CORS headers before the browser will send your actual request.
It depends on what your API does. For truly public APIs that serve read-only, non-sensitive data, a wildcard is fine. For any API that uses authentication, user data, or cookies, you should specify exact allowed origins instead. A wildcard combined with credentials is also blocked by browsers by design, so it won't work anyway when credentials are involved.
Several things can cause this. Your server might be returning an error (4xx or 5xx) before the CORS middleware runs, so the headers never get added. You might have a trailing slash mismatch in the origin value. Your OPTIONS preflight might be returning a 404. Or you're using a wildcard with credentials. Check the Network tab in DevTools, look at the actual response headers on both the OPTIONS and the main request, and trace exactly where the headers are missing.
The
Access-Control-Allow-Origin
header can only contain one value at a time, so you can't list multiple origins directly. Instead, read the incoming
Origin
request header on the server, check it against your allowlist, and if it matches, reflect that exact origin back in the response header. The Express example in this article shows exactly how to do this with a dynamic origin function in the cors middleware.