You've just seen it: "has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present."
This is one of the most Googled developer errors of all time — and understanding it once makes it trivial to fix every time.
What Is CORS?
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls which origins (domains) can make HTTP requests to your server.
Without CORS restrictions, any malicious website could silently make requests to your bank, email, or internal API using your logged-in session — a classic Cross-Site Request Forgery (CSRF) attack.
CORS is enforced by the browser, not the server. The server sets response headers saying "I allow these origins." The browser reads those headers and either delivers the response to your JavaScript code or blocks it.
The Same-Origin Policy
The Same-Origin Policy (SOP) is the foundation. A browser allows a page to freely make requests to the same origin it was loaded from:
Origin = scheme + host + port
https://example.com:443 → https://example.com:443 ✅ same origin
https://example.com → https://api.example.com ❌ different subdomain
https://example.com → http://example.com ❌ different scheme
https://example.com → https://example.com:8080 ❌ different port
CORS is the mechanism that relaxes this policy in a controlled way.
Simple vs Preflighted Requests
Not all cross-origin requests are treated equally.
Simple Requests
A request is "simple" if it meets all of these:
- Method:
GET,POST, orHEAD - Content-Type:
text/plain,multipart/form-data, orapplication/x-www-form-urlencoded - No custom headers beyond a small safe list
Simple requests go straight to the server. The browser checks the response's Access-Control-Allow-Origin header before handing the data to JavaScript.
Preflighted Requests
Anything else (JSON body, PUT/DELETE, Authorization header, etc.) triggers a preflight: the browser first sends an OPTIONS request to ask "is this allowed?" The server must respond with the right CORS headers before the actual request is made.
Browser → OPTIONS /api/data
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
Server → 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Browser → POST /api/data (actual request)
CORS Response Headers Reference
| Header | Purpose | Example |
|---|---|---|
Access-Control-Allow-Origin |
Which origins can read the response | https://app.example.com or * |
Access-Control-Allow-Methods |
Allowed HTTP methods (preflight) | GET, POST, PUT, DELETE, OPTIONS |
Access-Control-Allow-Headers |
Allowed request headers (preflight) | Content-Type, Authorization |
Access-Control-Allow-Credentials |
Allow cookies/auth headers | true |
Access-Control-Max-Age |
How long to cache the preflight (seconds) | 86400 |
Access-Control-Expose-Headers |
Extra headers JS can read | X-Total-Count, X-Request-Id |
Critical rule: If Access-Control-Allow-Credentials: true, you cannot use Access-Control-Allow-Origin: * — you must specify the exact origin.
How to Fix CORS Errors
Node.js / Express
// Install: npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
// Allow specific origin
app.use(cors({
origin: 'https://app.example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // allow cookies
maxAge: 86400, // cache preflight 24h
}));
// Dynamic origin whitelist
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
}));
// Handle preflight for all routes
app.options('*', cors());
Manual headers (without the cors package):
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
Python (FastAPI)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"], # or ["*"] to allow all
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization"],
max_age=86400,
)
Flask:
# Install: pip install flask-cors
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app, origins=["https://app.example.com"], supports_credentials=True)
# Per-route CORS
@app.route('/public')
@cross_origin() # allow all origins for this route only
def public():
return "open"
Go (net/http)
package main
import (
"net/http"
)
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
allowed := map[string]bool{
"https://app.example.com": true,
"https://admin.example.com": true,
}
if allowed[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin") // important for caching
}
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "86400")
// Handle preflight
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/data", handler)
http.ListenAndServe(":8080", corsMiddleware(mux))
}
With gorilla/mux:
// github.com/rs/cors is the standard Go CORS library
import "github.com/rs/cors"
c := cors.New(cors.Options{
AllowedOrigins: []string{"https://app.example.com"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 86400,
})
handler := c.Handler(mux)
PHP
<?php
// Set allowed origins dynamically
$allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowedOrigins, true)) {
header("Access-Control-Allow-Origin: $origin");
header("Vary: Origin");
}
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
header("Access-Control-Allow-Credentials: true");
header("Access-Control-Max-Age: 86400");
// Handle preflight — respond and stop
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
Laravel (config/cors.php):
return [
'paths' => ['api/*'],
'allowed_methods' => ['*'],
'allowed_origins' => ['https://app.example.com'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 86400,
'supports_credentials' => true,
];
Quick Reference: CORS Error → Fix
| Error Message | Root Cause | Fix |
|---|---|---|
No Access-Control-Allow-Origin header |
Server doesn't send CORS headers | Add CORS middleware on the server |
Access-Control-Allow-Origin: * with credentials |
Wildcard + credentials conflict | Use specific origin instead of * |
| Preflight response status 404/405 | OPTIONS route not handled |
Add OPTIONS handler or global preflight response |
Header not in Access-Control-Allow-Headers |
Custom header not whitelisted | Add the header name to allow_headers |
Blocked by Access-Control-Allow-Methods |
Method not listed | Add the method to allow_methods |
CORS error on localhost |
Localhost origin mismatch (http://localhost:3000 vs http://localhost:5173) |
Add all dev origins to the allowed list |
| CORS headers missing on error responses (4xx/5xx) | Middleware runs after error handler | Ensure CORS middleware is first in the chain |
Nginx / Apache: Reverse Proxy Fix
If your API doesn't set CORS headers, you can add them at the reverse proxy level:
Nginx:
location /api/ {
proxy_pass http://backend:8080;
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' 'Content-Type, Authorization';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
}
Apache (.htaccess):
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 "Content-Type, Authorization"
Header always set Access-Control-Allow-Credentials "true"
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule .* - [R=204,L]
6 Common CORS Mistakes
Using
*with credentials.Access-Control-Allow-Origin: *andAccess-Control-Allow-Credentials: truetogether are invalid — browsers reject it. Always specify the exact origin when using cookies or Authorization headers.Forgetting the
Vary: Originheader. When you set theAllow-Originheader dynamically (different values per request), you must also setVary: Origin— otherwise CDNs and proxies will cache one origin's response and serve it to others.Not handling
OPTIONSpreflight. If your framework or router doesn't respond toOPTIONSrequests, preflighted requests will fail. Add a globalOPTIONShandler that returns 204 before touching auth middleware.CORS middleware after auth middleware. If authentication runs before CORS headers are set, the browser never receives the CORS headers on a 401 response and shows a CORS error instead of the real auth error. Put CORS first.
Matching
localhostexactly.http://localhost:3000andhttp://localhost:5173are different origins. During development, add all ports you use, or allow the entirelocalhostpattern.Thinking CORS is a security measure on the server. CORS only restricts browser JavaScript. A server-to-server request (curl, Postman, fetch in Node.js) ignores CORS entirely. CORS does not protect your API — authentication does.
FAQ
Why do I get a CORS error in the browser but Postman works fine?
CORS is enforced by browsers, not by HTTP. Postman, curl, and server-to-server code don't send an Origin header and aren't subject to the browser's Same-Origin Policy.
Can I disable CORS in Chrome for development?
You can launch Chrome with --disable-web-security, but this disables all origin checks — don't browse real sites in that session. Better approach: add localhost to your server's allowed origins.
What does Access-Control-Allow-Origin: * mean?
It allows any origin to read the response. Safe for fully public APIs (no cookies, no user data). Dangerous if used with an API that relies on session cookies for auth, because any site can read the response.
My CORS headers are set but I still get an error — why?
Check: (1) Is the server sending them on the OPTIONS preflight too? (2) Are they present on error responses (4xx/5xx), not just 200 responses? (3) Is there a Vary: Origin header when using dynamic origins? (4) Is the exact origin string matching (protocol + host + port)?
What's the difference between CORS and CSRF?
CORS controls which origins can read responses in the browser. CSRF is an attack where a malicious site causes your browser to send a request to another site using your cookies. CORS partially mitigates CSRF (preflighted requests require explicit allowance), but proper CSRF protection requires tokens or SameSite=Strict cookies.
How do I allow all subdomains of my domain?
There's no wildcard subdomain support in the CORS spec. You must implement it server-side: check if the Origin header matches a regex like /^https:\/\/[a-z0-9-]+\.example\.com$/ and reflect the exact origin in Access-Control-Allow-Origin. Remember to add Vary: Origin.