How to Fix CORS Policy Error in JavaScript — Every Scenario Covered
"Access to fetch at '…' has been blocked by CORS policy" — this error stops more developers cold than almost any other. This guide covers every CORS scenario: browser fetch, Axios, local dev, production, proxies, and specific backend fixes for Node, Python, PHP, and more.
#1
most Googled JS network error
3
lines to fix on most backends
100%
server-side fix required
5 min
to understand CORS fully
The classic CORS error
Why CORS Exists
Browsers enforce the Same-Origin Policy: JavaScript can only make requests to the same origin (scheme + host + port) as the page it's running on. CORS (Cross-Origin Resource Sharing) is the mechanism that lets servers opt in to allow cross-origin requests.
Key insight
CORS is enforced by the browser, not the server. The server receives the request just fine. The browser blocks the JavaScript from accessing the response if the server doesn't send the right headers.
Same-origin means identical scheme + host + port
http://localhost:3000 and http://localhost:4000 are different origins. https://example.com and http://example.com are different. Subdomains count: api.example.com vs example.com is cross-origin.
The request still reaches the server
CORS does NOT prevent the request from being sent. The server processes it and responds. The browser then checks the response headers and decides whether to expose the data to JavaScript.
CORS is browser-only
curl, Postman, and server-to-server requests are not subject to CORS. Only browser-based JavaScript is restricted. This is why Postman works but your frontend gets blocked.
It is always a server-side fix
You cannot fully fix CORS from the frontend. The server must add the appropriate headers. The only frontend workaround is proxying requests through the same origin.
The Fix: Add Headers on Your Server
The server must include Access-Control-Allow-Origin in its response:
# Minimum (allows specific origin):
Access-Control-Allow-Origin: http://localhost:3000
# Or allow all origins (public APIs only):
Access-Control-Allow-Origin: *
# For requests with cookies/auth:
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Credentials: true
# Note: cannot use * with credentials!Fix by Backend Framework
const cors = require('cors');
// Quick fix: allow all origins (dev only)
app.use(cors());
// Production fix: specific origins
app.use(cors({
origin: ['https://myapp.com', 'https://www.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // remove if not using cookies
}));
// Handle preflight for all routes
app.options('*', cors());from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.com"], # or ["*"] for all
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)# 1. Install: pip install django-cors-headers
# 2. settings.py:
INSTALLED_APPS = [
...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # must be first!
'django.middleware.common.CommonMiddleware',
...
]
CORS_ALLOWED_ORIGINS = [
"https://myapp.com",
"http://localhost:3000",
]
# Or allow all (dev only):
CORS_ALLOW_ALL_ORIGINS = Truefunc corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://myapp.com")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}<?php
header("Access-Control-Allow-Origin: https://myapp.com");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
header("Access-Control-Allow-Credentials: true");
// Handle preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}server {
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;
add_header 'Access-Control-Allow-Credentials' 'true' always;
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Length' 0;
return 204;
}
proxy_pass http://localhost:8000;
}
}Fix for Local Development (Proxy)
During development, the easiest fix is to proxy requests through your frontend dev server — this makes the browser think everything is on the same origin.
/** @type {import('next').NextConfig} */
module.exports = {
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'http://localhost:8000/:path*',
},
];
},
};
// Now call /api/users instead of http://localhost:8000/usersexport default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^/api/, ''),
},
},
},
};{
"proxy": "http://localhost:8000"
}
// All unrecognized requests now proxy to your backendFix for Axios
Axios sends requests the same way as fetch, so the CORS headers still need to be set on the server. But there are Axios-specific settings for credentials:
No credentials
// Cookies not sent with Axios by default
axios.get('https://api.example.com/profile')Credentials enabled
// Include credentials (for session cookies)
axios.get('https://api.example.com/profile', {
withCredentials: true,
})
// Or globally for all requests:
axios.defaults.withCredentials = true;Preflight Requests — The Hidden CORS Issue
"Non-simple" requests trigger an automatic OPTIONS preflight. If your server doesn't handle OPTIONS, the real request never fires.
What triggers a preflight?
- • Custom headers like
Authorization - • Methods other than GET, HEAD, POST
- • Content-Type other than text/plain, multipart/form-data, application/x-www-form-urlencoded
- •
Content-Type: application/jsonalso triggers preflight
No OPTIONS handler
// Server returns 405 for OPTIONS — preflight fails
app.post('/api/data', handler);
// No OPTIONS handler → frontend fetch never completesOPTIONS handled
// Handle preflight explicitly
app.options('/api/data', cors()); // or:
app.options('*', cors()); // all routes
// Or use the cors() middleware globally before all routes
app.use(cors(corsOptions));
app.post('/api/data', handler);CORS with Cookies Checklist
Set credentials: "include" in fetch (or withCredentials: true in Axios)
Without this setting, cookies are never sent cross-origin. The browser strips them silently. Always set explicitly when your API depends on session cookies.
Set Access-Control-Allow-Credentials: true on server
The server must explicitly opt in to credentials. If this header is missing, the browser rejects the response even if the data is present.
Use explicit origin (NOT wildcard *)
The CORS spec forbids using * when credentials are enabled. The server must respond with the exact requesting origin (e.g., https://myapp.com), not a wildcard.
Set SameSite=None; Secure on the cookie
Modern browsers require SameSite=None; Secure for cookies sent cross-origin. Without this, cookies are silently dropped. Requires HTTPS — won't work on HTTP.
Verify your backend echoes the Origin header dynamically
For multi-origin setups, check the incoming Origin header on the server and echo back that specific origin in Access-Control-Allow-Origin. Hardcoding one origin breaks other legitimate clients.
CORS Headers Comparison
| Item | Simple Requests (no preflight) | Preflighted Requests |
|---|---|---|
| Methods allowed | GET, HEAD, POST only | Any method (PUT, DELETE, PATCH, etc.) |
| Headers allowed | Only standard headers | Any custom header (Authorization, X-Custom, etc.) |
| Content-Type | text/plain, form-encoded, multipart | application/json and anything else |
| Preflight sent | No — request goes directly | Yes — OPTIONS request sent first |
| Server must handle | Just the actual request | OPTIONS + the actual request |
| Example | fetch('/api', { method: 'GET' }) | fetch('/api', { method: 'POST', headers: { 'Authorization': 'Bearer ...' } }) |
| Item | Access-Control-Allow-Origin: * | Access-Control-Allow-Origin: specific |
|---|---|---|
| Use case | Public APIs, CDN assets, open data | Authenticated APIs, private data |
| Works with credentials | ❌ No — spec forbids it | ✅ Yes — required for cookie auth |
| Security level | Low — any site can read the response | High — only listed origins allowed |
| Browser support | ✅ All browsers | ✅ All browsers |
| Multiple origins | N/A — already allows all | Must check + echo Origin dynamically |
Debugging CORS Step by Step
Open DevTools Network tab and reproduce the error
Find the failing request. Check if the response has the Access-Control-Allow-Origin header. If the header is absent, the fix is entirely on the server. If it's present but wrong, check the value.
Check if there is a preflight OPTIONS request
In the Network tab, filter for "OPTIONS" requests. If the preflight returned a non-200 response or is missing headers, fix the OPTIONS handler on your server first.
Test the API directly with curl
Run: curl -H "Origin: http://localhost:3000" -I https://api.example.com/data. If Access-Control-Allow-Origin is in the curl response, the server is configured. If not, the server needs fixing.
Check for proxy/load balancer stripping headers
Sometimes CORS headers are added by the application but stripped by a reverse proxy (Nginx, CloudFront, etc.). Verify headers at each layer with curl --verbose.