You’ve built an API. It works perfectly in Postman. Then you call it from your frontend and get:

Ahicascsepsbrseeestneonbtfleootcnckhetdhaetbyr'ehCqtOutRepSsst:pe/dlairpceiys.:oeuxNraocmep'.lAec.cceosms'-Cfornotmroolr-iAglilnow'-hOtrtipgsi:n/'mhyeaapdpe.rcom'

CORS isn’t your API being broken. It’s your browser protecting users. Understanding why it exists makes fixing it straightforward.

The Same-Origin Policy

Browsers enforce a security rule: JavaScript on one origin can’t freely access resources from another origin.

An origin is: protocol + host + port

hhhhttttttttppppss:s::://m/maymypayaipap.pppm.p.yc.caocopmompm.:c8o0m80OOOOrrrriiiiggggiiiinnnnABCD(((dddiiiffffffeeerrreeennnttthpporosorttt)o)col)

Without this restriction, malicious JavaScript on evil.com could make requests to yourbank.com using your cookies, stealing your data.

How CORS Works

CORS (Cross-Origin Resource Sharing) is the controlled exception to same-origin policy. Servers opt-in to allowing cross-origin requests by sending specific headers.

Simple Requests

For GET/POST with standard headers, the browser makes the request directly and checks the response:

1
2
3
GET /api/users HTTP/1.1
Host: api.example.com
Origin: https://myapp.com

Server response:

1
2
3
4
5
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Content-Type: application/json

{"users": [...]}

The Access-Control-Allow-Origin header tells the browser “yes, myapp.com is allowed to read this response.”

Preflight Requests

For “non-simple” requests (PUT, DELETE, custom headers, JSON content-type), the browser first sends an OPTIONS request:

1
2
3
4
5
OPTIONS /api/users/123 HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type

Server must respond with permissions:

1
2
3
4
5
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400

Only then does the browser send the actual DELETE request.

Server-Side Configuration

Express.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const cors = require('cors');

// Allow specific origin
app.use(cors({
  origin: 'https://myapp.com',
  credentials: true
}));

// Allow multiple origins
app.use(cors({
  origin: ['https://myapp.com', 'https://staging.myapp.com'],
  credentials: true
}));

// Dynamic origin checking
app.use(cors({
  origin: (origin, callback) => {
    const allowed = ['https://myapp.com', 'https://staging.myapp.com'];
    if (!origin || allowed.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  }
}));

FastAPI (Python)

1
2
3
4
5
6
7
8
9
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://myapp.com", "https://staging.myapp.com"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Nginx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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' 'Authorization, Content-Type' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;
    
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Max-Age' 86400;
        add_header 'Content-Length' 0;
        return 204;
    }
    
    proxy_pass http://backend;
}

Common Mistakes

Wildcard with Credentials

1
2
3
# WRONG - browsers reject this combination
allow_origins=["*"],
allow_credentials=True,

You can’t use * (any origin) with credentials. The browser requires an explicit origin when cookies are involved. Specify exact origins instead.

Forgetting OPTIONS Handler

1
2
3
4
# Only handles GET - preflight fails
@app.get("/api/data")
def get_data():
    return {"data": "value"}

Your framework’s CORS middleware usually handles OPTIONS automatically. If you’re manually adding headers, remember to handle OPTIONS requests.

www vs non-www

1
2
# These are different origins!
allow_origins=["https://example.com"]  # Won't allow www.example.com

If users might access via both, allow both:

1
allow_origins=["https://example.com", "https://www.example.com"]

HTTP vs HTTPS

1
2
# Protocol matters
allow_origins=["https://myapp.com"]  # Won't allow http://myapp.com

Debugging CORS Issues

Browser Console

The error message tells you exactly what’s wrong:

NTRMoheeSeUqNtN'esueheArvieeoecvansdddcelgterutPtsehoUosieT-sowaaaCnfiddido'tedsdntthrthhnmrsecfeoeoeriattln'eedh-dAdleaoAicedrldlncnllgetXtotosi-owowCsaCe-O-luAdAORCssccrSotccinoeeghtmssier-ssnaoH--'dleCCe-aoohrAdnneslettalrrrdoooewillr-s--OAArnlliollgtooiwwna--'lHMleehoatewdhaeeoddrdessrmustnotbethewildcardwhencredentialsmodeis'include'

Network Tab

Check the actual request/response headers:

  1. Open DevTools → Network tab
  2. Find the failed request (might be OPTIONS)
  3. Check Response Headers for Access-Control-* headers
  4. Check if OPTIONS returns 200/204 or an error

curl Testing

Test from command line (no CORS enforcement):

1
2
3
4
5
6
7
# Test the actual endpoint
curl -i https://api.example.com/data

# Simulate preflight
curl -i -X OPTIONS https://api.example.com/data \
  -H "Origin: https://myapp.com" \
  -H "Access-Control-Request-Method: POST"

When CORS Doesn’t Apply

CORS is browser-enforced. These bypass it entirely:

  • Server-to-server requests (no browser involved)
  • Postman, curl, wget (not browsers)
  • Mobile apps (native HTTP clients)
  • Same-origin requests (no cross-origin)

If it works in Postman but not in the browser, it’s definitely CORS.

Proxy as Alternative

If you can’t modify the API’s CORS headers, proxy through your own backend:

Browser(YsoaumreBoarcikgeinnd)(Esxetrevrenra-ltoA-PsIerver,noCORS)
1
2
3
4
5
// Your backend proxies the request
app.get('/api/proxy/weather', async (req, res) => {
  const response = await fetch('https://external-api.com/weather');
  res.json(await response.json());
});

Now your frontend calls /api/proxy/weather — same origin, no CORS issues.


CORS errors aren’t bugs — they’re security features working correctly. The browser is asking “does this server want to allow this origin?” If the answer isn’t explicitly “yes,” the request fails.

Configure your allowed origins explicitly. Handle OPTIONS requests. Match protocols and subdomains exactly. The error messages tell you exactly what’s missing — read them carefully.