Why Developers Don't Understand CORS (And How to Fix It)
Meta Description: Developers don't understand CORS (2019) remains relevant today. Learn why Cross-Origin Resource Sharing confuses developers and how to implement it correctly in 2026.
TL;DR: CORS (Cross-Origin Resource Sharing) is one of the most misunderstood browser security mechanisms in web development. Despite a landmark 2019 discussion that highlighted widespread developer confusion, the same mistakes persist today. This article breaks down exactly why CORS trips developers up, what the browser is actually doing, and how to configure it correctly — without just slapping
Access-Control-Allow-Origin: *on everything and calling it a day.
The 2019 Wake-Up Call: Developers Don't Understand CORS
Back in 2019, a widely-shared blog post and subsequent Hacker News thread crystallized something that senior developers had been quietly observing for years: most developers fundamentally misunderstand CORS. The post resonated because it named a real, persistent problem — not just a beginner mistake, but a conceptual gap that affects experienced engineers too.
Seven years later, the situation hasn't dramatically improved. Stack Overflow still sees thousands of CORS-related questions monthly. "CORS error" remains one of the most Googled error messages in web development. And critically, the wrong fixes — disabling CORS entirely, using browser extensions to bypass it, or blindly copy-pasting permissive headers — are still the most common responses.
So what's actually going on? Let's build a real understanding from the ground up.
[INTERNAL_LINK: browser security fundamentals]
What CORS Actually Is (And What It Isn't)
Before diagnosing the misunderstanding, we need to establish the correct mental model.
CORS Is a Browser Feature, Not a Server Feature
This is the single most important thing to understand, and it's where most confusion begins.
CORS does not protect your server. It protects users.
When your browser makes a cross-origin request — say, JavaScript on app.example.com calls an API at api.otherdomain.com — the browser enforces a policy called the Same-Origin Policy (SOP). This policy exists to prevent malicious websites from making authenticated requests to other sites on your behalf.
CORS is the mechanism that allows servers to relax that policy in a controlled way. The server tells the browser: "Yes, I'm okay receiving requests from app.example.com." The browser then allows the response through.
Key insight: If you disable CORS checks using a browser extension or a proxy, you're not "fixing" anything — you're bypassing a user-protection mechanism. Your API is still just as accessible; you've just removed the safety net for your users.
The Same-Origin Policy: A Quick Refresher
Two URLs share the same origin if they match on all three of:
- Protocol (http vs https)
- Domain (example.com vs otherdomain.com)
- Port (3000 vs 8080)
| URL Comparison | Same Origin? | Reason |
|---|---|---|
https://example.com vs https://example.com/api
|
✅ Yes | Same protocol, domain, port |
https://example.com vs http://example.com
|
❌ No | Different protocol |
https://example.com vs https://api.example.com
|
❌ No | Different subdomain |
https://example.com:3000 vs https://example.com:8080
|
❌ No | Different port |
https://example.com vs https://otherdomain.com
|
❌ No | Different domain |
Why Developers Get CORS Wrong: The Root Causes
The 2019 discourse identified several recurring patterns of misunderstanding. Here's a structured breakdown.
1. Treating CORS as a Server-Side Security Mechanism
Many developers add CORS headers thinking they're protecting their API. They're not — at least not in the way they think. A curl request, a Postman call, or a server-to-server request completely bypasses CORS. The browser enforces CORS; nothing else does.
The practical consequence: Relying on CORS to secure your API is a critical mistake. You still need proper authentication, authorization, rate limiting, and input validation. CORS is not a substitute for any of these.
2. Confusing Simple Requests and Preflight Requests
This is where even experienced developers stumble. CORS has two distinct flows:
Simple Requests
A request is "simple" if it meets all of these criteria:
- Method is
GET,POST, orHEAD - Content-Type is
application/x-www-form-urlencoded,multipart/form-data, ortext/plain - No custom headers
Simple requests are sent directly, and the browser checks the response headers afterward.
Preflight Requests
Any request that doesn't meet the "simple" criteria triggers a preflight — an automatic OPTIONS request the browser sends before your actual request. The browser is asking the server: "Are you okay with this type of request from this origin?"
Why this matters: Developers see a mysterious OPTIONS request in their network tab and have no idea where it came from. They block it on their server (often in a firewall rule or middleware), which causes their actual request to fail. The error message is cryptic. Chaos ensues.
// This will trigger a preflight because of the custom header
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // Not in the "simple" list
'Authorization': 'Bearer token123' // Custom header = preflight
},
body: JSON.stringify({ key: 'value' })
});
3. The Access-Control-Allow-Origin: * Trap
The nuclear option. When developers encounter a CORS error, the fastest fix they find is setting Access-Control-Allow-Origin: * — allowing any origin. Sometimes this is fine (for truly public APIs). Often, it's a security risk.
The critical gotcha: You cannot use a wildcard with Access-Control-Allow-Credentials: true. If you need to send cookies or HTTP authentication, you must specify exact origins. Browsers will reject the wildcard + credentials combination.
// This will FAIL — browsers block this combination
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
// This works correctly
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
4. Misreading CORS Error Messages
Browser CORS error messages are notoriously unhelpful. A typical Chrome error looks like:
"Access to fetch at 'https://api.example.com' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource."
Developers often misread this as "the server rejected my request." In reality, the server may have returned a perfectly valid 200 response — the browser is blocking JavaScript from reading it because the response lacked the appropriate CORS headers.
This distinction matters enormously for debugging.
[INTERNAL_LINK: browser developer tools guide]
How to Implement CORS Correctly: A Practical Guide
The Headers You Actually Need to Know
| Header | Direction | Purpose |
|---|---|---|
Access-Control-Allow-Origin |
Response | Which origins are permitted |
Access-Control-Allow-Methods |
Response | Which HTTP methods are allowed |
Access-Control-Allow-Headers |
Response | Which request headers are allowed |
Access-Control-Allow-Credentials |
Response | Whether cookies/auth can be sent |
Access-Control-Max-Age |
Response | How long to cache preflight results |
Access-Control-Expose-Headers |
Response | Which response headers JS can read |
Origin |
Request | The origin making the request |
Framework-Specific Implementation
Node.js / Express
The cors npm package is the standard solution and handles most use cases well. It's actively maintained and well-documented.
const cors = require('cors');
const corsOptions = {
origin: ['https://app.example.com', 'https://admin.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // Cache preflight for 24 hours
};
app.use(cors(corsOptions));
Honest assessment: The cors package is excellent for most use cases, but its dynamic origin validation (passing a function) can be tricky to get right. Test thoroughly with both allowed and disallowed origins.
Python / Django
django-cors-headers is the go-to solution for Django projects.
# settings.py
INSTALLED_APPS = [
...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # Must be before CommonMiddleware
'django.middleware.common.CommonMiddleware',
...
]
CORS_ALLOWED_ORIGINS = [
"https://app.example.com",
"https://admin.example.com",
]
CORS_ALLOW_CREDENTIALS = True
Nginx Configuration
If you're handling CORS at the reverse proxy level:
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
add_header 'Access-Control-Allow-Credentials' 'true';
proxy_pass http://backend;
}
Common CORS Mistakes and How to Fix Them
Mistake #1: Not Handling OPTIONS Requests
Your server must respond to OPTIONS requests (preflight) with a 200 or 204 status and the appropriate headers. Many frameworks don't do this automatically.
Fix: Explicitly handle OPTIONS in your routing, or use middleware that does it for you.
Mistake #2: Adding CORS Headers Only to Success Responses
If your API returns a 401 or 500 error, it needs CORS headers too. Otherwise, the browser blocks the error response and your frontend gets a generic CORS error instead of the actual error message.
Fix: Add CORS headers to all responses, including errors. Configure this at the middleware level, not in individual route handlers.
Mistake #3: Caching Issues with Dynamic Origins
If you support multiple origins and dynamically set Access-Control-Allow-Origin based on the request's Origin header, you must also set:
Vary: Origin
Without this, a CDN or browser cache might serve a response with the wrong origin header to a different requester.
Mistake #4: Forgetting Credentials in Fetch Calls
Even if your server correctly sets Access-Control-Allow-Credentials: true, your frontend must explicitly opt in:
fetch('https://api.example.com/data', {
credentials: 'include' // Don't forget this!
});
Useful Tools for Debugging CORS
Browser DevTools
Your first stop. The Network tab shows all requests including preflight OPTIONS calls. The Console shows the specific CORS error. Chrome and Firefox both provide reasonably detailed CORS error messages as of 2026.
Hoppscotch
A free, open-source API testing tool (a Postman alternative). Useful for testing your API endpoints directly without browser CORS restrictions, helping you isolate whether the issue is server configuration or browser behavior.
Honest take: Great for quick testing, though Postman still has a more mature feature set for complex workflows.
CORS Tester by Requestly
Requestly offers a browser extension that can intercept and modify requests/responses, which is invaluable for debugging CORS issues in development. It lets you add or modify headers without changing your server code.
Honest take: Very useful for local development debugging. Don't use it as a permanent fix in production — address the root cause on your server.
curl for Baseline Testing
# Test a preflight request manually
curl -X OPTIONS https://api.example.com/data \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Authorization, Content-Type" \
-v
If your server doesn't return the right headers to curl, it definitely won't work in a browser.
[INTERNAL_LINK: API debugging techniques]
Key Takeaways
- CORS protects users, not servers. It's a browser enforcement mechanism. Server-to-server requests and tools like curl ignore it entirely.
- The Same-Origin Policy is the baseline. CORS is how you selectively relax it.
-
Preflight requests are automatic. Any non-simple request triggers an
OPTIONSpreflight. Your server must handle it. -
Wildcards and credentials don't mix. You can't use
Access-Control-Allow-Origin: *withAccess-Control-Allow-Credentials: true. - Add CORS headers to all responses, including errors. Middleware-level configuration is safer than per-route.
-
Always set
Vary: Originwhen dynamically reflecting the request origin. - CORS is not a security tool. Implement proper authentication and authorization separately.
Final Thoughts and CTA
The 2019 conversation about developer CORS confusion was valuable precisely because it named a real, systemic problem — one that stems from the browser's somewhat opaque enforcement of a security model that most developers never explicitly learned. The good news is that once you internalize the correct mental model (browser feature, not server security), everything else clicks into place.
Your action items:
- Audit your current CORS configuration — are you using
*where you shouldn't be? - Check that your server handles
OPTIONSrequests correctly - Add
Vary: Originif you're dynamically setting allowed origins - Make sure CORS headers appear on all responses, not just successful ones
If you found this helpful, check out our related guides on [INTERNAL_LINK: web security best practices] and [INTERNAL_LINK: REST API design principles]. And if you're setting up a new project, consider starting with a well-configured CORS middleware from day one rather than retrofitting it later.
Frequently Asked Questions
Q1: Why does my CORS request work in Postman but fail in the browser?
This is the clearest illustration that CORS is a browser feature. Postman doesn't enforce the Same-Origin Policy — it's not a browser. When your request works in Postman but fails in Chrome or Firefox, it means your server is responding correctly but isn't returning the CORS headers the browser requires. Fix the server-side CORS configuration; don't try to replicate the Postman environment in production.
Q2: Is it safe to use Access-Control-Allow-Origin: *?
It depends on your use case. For truly public APIs that don't handle authentication or sensitive data (think: a public weather API), a wildcard is fine and appropriate. For any API that handles user data, authentication tokens, or cookies, you should specify exact allowed origins. The wildcard also can't be used with Access-Control-Allow-Credentials: true, so if you need credentials, you have no choice but to be specific.
Q3: What's the difference between a CORS error and a network error?
A CORS error means the request reached the server and got a response, but the browser is blocking JavaScript from reading that response due to missing or incorrect CORS headers. A network error means the request never reached the server (DNS failure, server down, firewall block, etc.). You can distinguish them in the browser DevTools — a CORS error will typically show a 200 or other status in the Network tab but still trigger a console error.
Q4: Do I need CORS for same-origin requests?
No. CORS only applies to cross-origin requests. If your frontend and API are on the same protocol, domain, and port, the Same-Origin Policy doesn't restrict them and CORS
Top comments (0)