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

Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
1

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.

2

The Fix: Add Headers on Your Server

The server must include Access-Control-Allow-Origin in its response:

httprequired-cors-headers.http
# 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!
3

Fix by Backend Framework

javascriptexpress-cors.js
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());
pythonfastapi-cors.py
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=["*"],
)
pythondjango-cors.py
# 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 = True
gogo-cors.go
func 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)
    })
}
phpcors.php
<?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();
}
nginxnginx-cors.conf
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;
    }
}
4

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.

javascriptnext.config.js
/** @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/users
javascriptvite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^/api/, ''),
      },
    },
  },
};
jsonpackage.json
{
  "proxy": "http://localhost:8000"
}
// All unrecognized requests now proxy to your backend
5

Fix 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

❌ Bad
// Cookies not sent with Axios by default
axios.get('https://api.example.com/profile')

Credentials enabled

✅ Good
// Include credentials (for session cookies)
axios.get('https://api.example.com/profile', {
  withCredentials: true,
})

// Or globally for all requests:
axios.defaults.withCredentials = true;
6

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/json also triggers preflight

No OPTIONS handler

❌ Bad
// Server returns 405 for OPTIONS — preflight fails
app.post('/api/data', handler);
// No OPTIONS handler → frontend fetch never completes

OPTIONS handled

✅ Good
// 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);
7

CORS with Cookies Checklist

1

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.

2

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.

3

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.

4

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.

5

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.

8

CORS Headers Comparison

ItemSimple Requests (no preflight)Preflighted Requests
Methods allowedGET, HEAD, POST onlyAny method (PUT, DELETE, PATCH, etc.)
Headers allowedOnly standard headersAny custom header (Authorization, X-Custom, etc.)
Content-Typetext/plain, form-encoded, multipartapplication/json and anything else
Preflight sentNo — request goes directlyYes — OPTIONS request sent first
Server must handleJust the actual requestOPTIONS + the actual request
Examplefetch('/api', { method: 'GET' })fetch('/api', { method: 'POST', headers: { 'Authorization': 'Bearer ...' } })
ItemAccess-Control-Allow-Origin: *Access-Control-Allow-Origin: specific
Use casePublic APIs, CDN assets, open dataAuthenticated APIs, private data
Works with credentials❌ No — spec forbids it✅ Yes — required for cookie auth
Security levelLow — any site can read the responseHigh — only listed origins allowed
Browser support✅ All browsers✅ All browsers
Multiple originsN/A — already allows allMust check + echo Origin dynamically
9

Debugging CORS Step by Step

1

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.

2

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.

3

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.

4

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.

Frequently Asked Questions