CORS Explained

Everything developers need to know — how CORS works, headers, preflight requests, common errors, and fixes

HTTP & APIsJune 29, 202620 min readBy Keyur Patel

You've seen the error. Red text in the console: "Access to fetch at ... has been blocked by CORS policy." You panic. You google. You add Access-Control-Allow-Origin: * to your server and move on. But you never really understood why it happened or what CORS actually does.

That's fine — most developers go through this. CORS is one of those things that blocks you at the worst possible time (usually during a demo) and feels like the browser is fighting against you. But once you understand the mechanism, it clicks and you never get stuck on it again.

This guide walks through everything: what CORS is, why it exists, how the browser enforces it, what preflight requests are, every CORS header explained, and how to fix the most common errors you'll hit in real projects.

What is CORS?

CORS stands for Cross-Origin Resource Sharing. It's a mechanism built into browsers that controls whether JavaScript running on one origin (e.g., https://myapp.com) is allowed to read responses from a different origin (e.g., https://api.myapp.com).

An "origin" is the combination of protocol + domain + port. These are all different origins:

https://myapp.com          ← Origin A
https://api.myapp.com      ← Origin B (different subdomain)
http://myapp.com           ← Origin C (different protocol)
https://myapp.com:3000     ← Origin D (different port)
https://myapp.com          ← Same as Origin A ✓

When your frontend on Origin A makes a fetch request to Origin B, the browser says: "hold on, that's a cross-origin request. Let me check if Origin B allows this." It looks at the response headers from Origin B to decide.

Key point: CORS is enforced by the browser, not the server. The server just sends headers that tell the browser what's allowed. If you call your API from Postman, curl, or another server — CORS never applies. It's purely a browser security feature.

Why CORS Exists: The Same-Origin Policy

To understand CORS, you need to understand the problem it solves. Browsers have a rule called the Same-Origin Policy (SOP) that's been around since the mid-90s. It says:

"JavaScript on one origin cannot read responses from a different origin."

Why? Imagine there's no Same-Origin Policy. You visit evil.com, and it runs JavaScript that:

  • Fetches https://yourbank.com/account/balance — using your logged-in cookies
  • Reads the response (your bank balance, transactions, everything)
  • Sends that data to the attacker's server

The Same-Origin Policy prevents this. Your browser will send the request (with cookies), but it won't let evil.com's JavaScript read the response. The data stays protected.

But here's the problem: legitimate cross-origin requests are everywhere. Your frontend on app.example.com needs to call your API on api.example.com. A React app on localhost:3000 needs to hit your Express server on localhost:8080.

That's where CORS comes in. It's an exception system that lets servers explicitly opt in to cross-origin access. The server says "I trust requests from these specific origins" by including CORS headers in its response.

How CORS Works: Simple vs Preflight Requests

Not all cross-origin requests are handled the same way. The browser divides them into two categories: simple requests and preflight requests.

Simple Requests

A request is "simple" when it meets ALL of these conditions:

  • Method is GET, HEAD, or POST
  • Only uses "safe" headers: Accept, Accept-Language, Content-Language, Content-Type
  • Content-Type (if set) is one of: application/x-www-form-urlencoded, multipart/form-data, or text/plain

For simple requests, the browser sends the request directly and then checks the response headers to decide whether the JavaScript is allowed to read the response.

Preflight Requests (Non-Simple)

Everything else triggers a preflight. That means:

  • Methods: PUT, DELETE, PATCH
  • Custom headers like Authorization, X-Custom-Header
  • Content-Type of application/json (yes, this triggers preflight!)

Before sending the actual request, the browser first sends an OPTIONS request asking: "Hey server, are you cool with a PUT request from this origin, with these headers?" Only if the server responds with the right CORS headers does the browser proceed with the actual request.

CORS Request Flow

Here's what happens step by step for both simple and preflight requests:

Simple Request Flow

Browser (https://myapp.com)              Server (https://api.myapp.com)
         │                                          │
         │  GET /users                              │
         │  Origin: https://myapp.com               │
         │ ────────────────────────────────────────► │
         │                                          │
         │  200 OK                                  │
         │  Access-Control-Allow-Origin: https://myapp.com
         │ ◄──────────────────────────────────────── │
         │                                          │
         │  ✓ Origin matches → JS can read response │
         │                                          │

Preflight Request Flow

Browser (https://myapp.com)              Server (https://api.myapp.com)
         │                                          │
         │  OPTIONS /users                          │  ← Preflight
         │  Origin: https://myapp.com               │
         │  Access-Control-Request-Method: DELETE    │
         │  Access-Control-Request-Headers: Authorization
         │ ────────────────────────────────────────► │
         │                                          │
         │  204 No Content                          │  ← Server approves
         │  Access-Control-Allow-Origin: https://myapp.com
         │  Access-Control-Allow-Methods: GET, POST, DELETE
         │  Access-Control-Allow-Headers: Authorization
         │  Access-Control-Max-Age: 86400           │
         │ ◄──────────────────────────────────────── │
         │                                          │
         │  DELETE /users/123                        │  ← Actual request
         │  Origin: https://myapp.com               │
         │  Authorization: Bearer token123          │
         │ ────────────────────────────────────────► │
         │                                          │
         │  200 OK                                  │
         │  Access-Control-Allow-Origin: https://myapp.com
         │ ◄──────────────────────────────────────── │
         │                                          │

Notice: the preflight is invisible to your JavaScript code. You just write fetch()and the browser handles the OPTIONS request behind the scenes. But you'll see it in the Network tab — and if it fails, your actual request never fires.

CORS Headers Explained

Six response headers control CORS behavior. Here's each one broken down — what it does, why you need it, where to set it, and who sends it.

• Access-Control-Allow-Origin

What: Tells the browser which origins are allowed to read the response. This is the primary CORS header — without it, cross-origin requests are always blocked.

Why: This is the gate. If the requesting origin doesn't match this header, the browser blocks JavaScript from accessing the response body, headers, or status.

Where: Every cross-origin response. Set on the server — either in your application code (Express middleware, Next.js headers) or reverse proxy (Nginx, Cloudflare).

Set by: Server (response header)

Access-Control-Allow-Origin: https://myapp.com    ← Allow specific origin
Access-Control-Allow-Origin: *                     ← Allow any origin (public APIs)

// ⚠️ You CANNOT list multiple origins directly:
Access-Control-Allow-Origin: https://a.com, https://b.com  ← INVALID

// Instead, dynamically set based on request Origin header:
// if (allowedOrigins.includes(req.headers.origin)) {
//   res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
// }

• Access-Control-Allow-Methods

What: Lists which HTTP methods are allowed for cross-origin requests. Sent in the preflight response.

Why: Even if the origin is allowed, the browser checks whether the specific method (DELETE, PUT, PATCH) is permitted. Simple methods (GET, POST, HEAD) don't need this — but it's good practice to include them.

Where: Preflight (OPTIONS) responses. Required when your API uses anything beyond GET/POST.

Set by: Server (preflight response)

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS

• Access-Control-Allow-Headers

What: Lists which request headers the client is allowed to send in cross-origin requests.

Why: Custom headers like Authorization, X-Request-ID, or Content-Type: application/json require explicit permission. Without this, the preflight fails and your request never fires.

Where: Preflight responses. Add every custom header your frontend sends.

Set by: Server (preflight response)

Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-ID

• Access-Control-Allow-Credentials

What: Tells the browser whether to include cookies, authorization headers, or TLS client certificates in cross-origin requests.

Why: By default, cross-origin requests don't send cookies. If your API uses cookie-based auth (sessions), you must set this to true AND the frontend must set credentials: 'include' in the fetch call.

Where: Both preflight and actual responses. Required for cookie-based authentication across origins.

Set by: Server (response header)

Access-Control-Allow-Credentials: true

// ⚠️ IMPORTANT: Cannot use wildcard origin with credentials!
// This combination is INVALID:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
// Browser rejects it. Must use specific origin when credentials are enabled.

• Access-Control-Max-Age

What: How long (in seconds) the browser can cache the preflight response. During this time, repeat requests skip the OPTIONS call.

Why: Preflight adds latency — an extra round trip before every non-simple request. Caching the preflight result means subsequent requests are fast. Without it, every single request gets double round-tripped.

Where: Preflight responses. Set high for stable APIs (86400 = 24 hours). Set low during development.

Set by: Server (preflight response)

Access-Control-Max-Age: 86400   ← Cache preflight for 24 hours
Access-Control-Max-Age: 600     ← Cache for 10 minutes (during development)
Access-Control-Max-Age: -1      ← Disable caching (every request triggers preflight)

• Access-Control-Expose-Headers

What: Lists which response headers JavaScript is allowed to read. By default, only 6 "safe" response headers are accessible to frontend code.

Why: Even if the server sends custom response headers (X-Total-Count, X-Request-ID, ETag), the browser hides them from JavaScript unless they're explicitly exposed.

Where: Actual (non-preflight) responses. Add any custom response headers your frontend needs to read.

Set by: Server (response header)

Access-Control-Expose-Headers: X-Total-Count, X-Request-ID, ETag

// Default "safe" headers always readable (no need to expose):
// Cache-Control, Content-Language, Content-Length,
// Content-Type, Expires, Pragma

Preflight Requests (OPTIONS) Deep Dive

Preflight requests cause more confusion than any other part of CORS. Let me clear it up.

What triggers a preflight?

Any of these conditions make a request "non-simple" and trigger preflight:

ConditionExamplePreflight?
GET with no custom headersfetch('/api/users')No
POST with form datafetch('/api', { method: 'POST', body: formData })No
POST with JSON bodyfetch('/api', { headers: { 'Content-Type': 'application/json' } })Yes ⚠️
Any request with Authorization headerfetch('/api', { headers: { 'Authorization': 'Bearer ...' } })Yes
PUT or DELETE methodfetch('/api/users/1', { method: 'DELETE' })Yes
Custom headers (X-*)fetch('/api', { headers: { 'X-Custom': 'value' } })Yes

What does a preflight look like?

// Preflight REQUEST (sent automatically by browser)
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type

// Preflight RESPONSE (your server must handle this)
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

Common preflight gotcha

If your server doesn't handle OPTIONS requests, the preflight gets a 404 or 405, and the browser blocks the actual request. Many routing frameworks need explicit OPTIONS handling:

// Express: cors middleware handles OPTIONS automatically
// But if you're doing manual CORS:
app.options('*', (req, res) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin)
  res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE')
  res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization')
  res.sendStatus(204)
})

Code Examples: Setting Up CORS

Here's how to configure CORS in the most common server environments. Copy, adapt, and you're done.

Express.js (Node.js)

// Option 1: Using the cors package (recommended)
const express = require('express')
const cors = require('cors')
const app = express()

// Allow specific origins
const corsOptions = {
  origin: ['https://myapp.com', 'https://staging.myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Total-Count', 'X-Request-ID'],
  credentials: true,
  maxAge: 86400,  // Cache preflight for 24 hours
}

app.use(cors(corsOptions))

// Option 2: Dynamic origin (check against allowed list)
app.use(cors({
  origin: (origin, callback) => {
    const allowed = ['https://myapp.com', 'https://admin.myapp.com']
    if (!origin || allowed.includes(origin)) {
      callback(null, true)
    } else {
      callback(new Error('Not allowed by CORS'))
    }
  },
  credentials: true,
}))

// Option 3: Manual headers (no package needed)
app.use((req, res, next) => {
  const allowed = ['https://myapp.com']
  const origin = req.headers.origin
  if (allowed.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin)
    res.setHeader('Access-Control-Allow-Credentials', 'true')
  }
  if (req.method === 'OPTIONS') {
    res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE')
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization')
    res.setHeader('Access-Control-Max-Age', '86400')
    return res.sendStatus(204)
  }
  next()
})

Next.js (App Router)

// next.config.mjs — Set CORS headers globally
const nextConfig = {
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          { key: 'Access-Control-Allow-Origin', value: 'https://myapp.com' },
          { key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE,OPTIONS' },
          { key: 'Access-Control-Allow-Headers', value: 'Content-Type,Authorization' },
          { key: 'Access-Control-Max-Age', value: '86400' },
        ],
      },
    ]
  },
}
export default nextConfig

// --- OR --- Route handler level (app/api/users/route.js)
export async function GET(request) {
  const data = await getUsers()
  return Response.json(data, {
    headers: {
      'Access-Control-Allow-Origin': 'https://myapp.com',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    },
  })
}

export async function OPTIONS() {
  return new Response(null, {
    status: 204,
    headers: {
      'Access-Control-Allow-Origin': 'https://myapp.com',
      'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE',
      'Access-Control-Allow-Headers': 'Content-Type,Authorization',
      'Access-Control-Max-Age': '86400',
    },
  })
}

Nginx

# Nginx CORS configuration
server {
    location /api/ {
        # Handle preflight
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://myapp.com' always;
            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;
        }

        # Actual requests
        add_header 'Access-Control-Allow-Origin' 'https://myapp.com' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Access-Control-Expose-Headers' 'X-Total-Count' always;

        proxy_pass http://backend:3000;
    }
}

Frontend: fetch() with CORS

// Simple cross-origin request (no preflight)
const res = await fetch('https://api.myapp.com/users')
const data = await res.json()

// With credentials (cookies sent cross-origin)
const res = await fetch('https://api.myapp.com/users', {
  credentials: 'include',  // ← Send cookies cross-origin
})

// Non-simple request (triggers preflight)
const res = await fetch('https://api.myapp.com/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',   // ← Triggers preflight
    'Authorization': 'Bearer token123',    // ← Triggers preflight
  },
  body: JSON.stringify({ name: 'Alice' }),
  credentials: 'include',
})

// ⚠️ You cannot set mode: 'no-cors' and read the response
// mode: 'no-cors' makes the response "opaque" — JS can't read it
const res = await fetch('https://api.example.com/data', {
  mode: 'no-cors',  // ← Response body is empty/inaccessible!
})

Common CORS Errors and How to Fix Them

These are the CORS errors you'll actually encounter in real projects. I've debugged all of them at some point — here's the fix for each one.

Error 1: "No 'Access-Control-Allow-Origin' header is present"

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

Cause: The server response doesn't include any CORS headers at all.

Fix: Add Access-Control-Allow-Origin to your server response. The fix is always on the server side.

Error 2: "The value of 'Access-Control-Allow-Origin' header must not be wildcard '*' when credentials mode is 'include'"

Cause: You set Access-Control-Allow-Origin: * but also have credentials: 'include' on the frontend or Access-Control-Allow-Credentials: true on the server.

Fix: Replace the wildcard with the specific requesting origin. Dynamically set it from the Origin request header (after validating against your allowed list).

Error 3: "Method DELETE is not allowed by Access-Control-Allow-Methods"

Cause: Your preflight response doesn't include the HTTP method being used in the Access-Control-Allow-Methods header.

Fix: Add the method to your CORS config: Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS

Error 4: "Request header field 'authorization' is not allowed by Access-Control-Allow-Headers"

Cause: You're sending a custom header (Authorization, X-Custom-Header) that isn't listed in Access-Control-Allow-Headers.

Fix: Add the header to your preflight response: Access-Control-Allow-Headers: Content-Type, Authorization

Error 5: Preflight returns 404 or 405

Console: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

Cause: Your server isn't handling OPTIONS requests. The router returns 404 (no route) or 405 (method not allowed).

Fix: Add an OPTIONS handler to your routes, or use CORS middleware that handles it automatically. Return 204 No Content with CORS headers.

Error 6: CORS works on GET but fails on POST (JSON)

Cause: GET is a simple request (no preflight). But POST with Content-Type: application/json triggers a preflight, and your server doesn't handle it.

Fix: Ensure your CORS config responds to OPTIONS requests and allows the Content-Type header.

Error 7: Works in development, fails in production

Cause: Usually one of: (1) Your proxy/CDN strips CORS headers, (2) Origin URL changed (http vs https, www vs non-www), (3) CORS is set on the dev server but not the production reverse proxy.

Fix: Check the exact origin in production. Verify your Nginx/Cloudflare/CDN passes through CORS headers. Use curl -I to test.

Error 8: "Works in Postman but not the browser"

Cause: Postman doesn't enforce CORS — it's not a browser. CORS is a browser-only security mechanism. If it works in Postman but not the browser, the issue is specifically missing/incorrect CORS headers.

Fix: Don't use Postman to test CORS. Use the browser Network tab to see actual preflight requests and response headers. Fix the headers on your server.

CORS vs CSRF vs CSP: What's the Difference?

These three acronyms get confused constantly. They're all browser security mechanisms, but they solve completely different problems.

AspectCORSCSRFCSP
What it isResponse header mechanismAttack type (and its prevention)Response header policy
Problem it solvesControls who can READ your API responsesPrevents unauthorized actions (state changes)Controls what resources a page can LOAD
Enforced byBrowser (checks response headers)Server (validates tokens/origin)Browser (checks response header)
Configured onServer (response headers)Server (generates/validates tokens)Server (response header)
Protects againstUnauthorized data readingForged form submissions, unwanted actionsXSS, code injection, data exfiltration
ExampleAPI blocks requests from evil.comBank requires CSRF token for transfersPage blocks inline scripts and external JS
Key headerAccess-Control-Allow-OriginX-CSRF-Token (custom), SameSite cookieContent-Security-Policy
Not a replacement forAuthentication/authorizationCORS (different problem)Input validation, CORS

In short: CORS = who can read my data. CSRF = preventing unwanted actions. CSP = what my page is allowed to load. You usually need all three in a production app.

CORS Best Practices

After spending hours debugging CORS on various projects, these are the rules I follow:

Always specify exact origins — avoid wildcard in production. * is fine for truly public, unauthenticated APIs. For everything else, whitelist specific origins.

Set Access-Control-Max-Age for stable APIs. 86400 seconds (24 hours) eliminates redundant preflight requests. Huge latency win for SPAs making many API calls.

Use a CORS middleware/library instead of manual headers. The cors npm package, Django CORS headers, and Spring CORS config all handle edge cases you'll miss manually.

Always handle OPTIONS requests explicitly. If your framework doesn't auto-handle them, add a catch-all OPTIONS route that returns 204 with CORS headers.

Validate the Origin header server-side. Don't blindly reflect the Origin header — that's as insecure as using *. Check against an explicit allowlist.

Set the Vary: Origin header when dynamically setting origins. Prevents CDNs from caching a response with one origin and serving it to another. Critical for multi-origin setups.

Only expose headers the frontend actually needs. Don't use Access-Control-Expose-Headers: * — list specific headers to maintain least-privilege.

Test CORS in the browser, not in Postman. Postman doesn't enforce CORS. Always verify with actual browser requests during development.

Keep credentials: 'include' only where needed. It requires specific origins (not wildcard), adds complexity, and sends cookies you might not intend to send.

Document your CORS policy alongside your API docs. Tell consumers which origins are allowed, whether credentials are supported, and any rate limits on preflight.

Common CORS Mistakes

I've made most of these at one point or another. Save yourself the debugging time.

Trying to fix CORS on the frontend. Adding headers to your fetch request doesn't fix CORS. The fix is always server-side. Your frontend can't override browser security policies.

Using a CORS proxy in production. Services like cors-anywhere are fine for demos but not production. They add latency, are unreliable, and expose your traffic to a third party.

Setting Access-Control-Allow-Origin to multiple origins in one header. The spec only allows a single origin value (or *). To support multiple, dynamically check the Origin header and reflect the matching one.

Blindly reflecting the Origin header without validation. If you just echo back whatever Origin the request sends, any website can access your API. Always validate against a whitelist.

Forgetting to add CORS headers on error responses. If your server throws a 500 without CORS headers, the browser shows a CORS error instead of the actual error. Always set CORS headers even on errors.

Thinking CORS replaces authentication. CORS controls which origins can call your API from a browser. It doesn't authenticate or authorize requests. You still need proper auth on every endpoint.

Disabling CORS entirely with a browser extension. Great for local debugging, terrible habit. You'll ship code that only works with the extension enabled and not in any real browser.

Not setting Vary: Origin with dynamic origins. Without it, a CDN might cache a response with Origin A's allowed-origin header and serve it to requests from Origin B, breaking CORS for B.

Frequently Asked Questions

What is CORS?
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls which websites can make requests to your server. It uses HTTP headers to tell the browser whether a cross-origin request is allowed. Without CORS headers, browsers block JavaScript from reading responses from different domains.
Why does CORS exist?
CORS exists because of the Same-Origin Policy — a security rule that prevents malicious websites from reading data from other sites using your browser's cookies. Without it, any website could make authenticated requests to your bank, email, or internal APIs and steal your data.
What is a preflight request?
A preflight request is an OPTIONS request the browser automatically sends before certain cross-origin requests. It asks the server 'are you okay with this type of request?' before sending the actual request. Preflight happens for non-simple requests — those using methods like PUT/DELETE, custom headers, or JSON content type.
How do I fix 'Access-Control-Allow-Origin' error?
Add the Access-Control-Allow-Origin header to your server's response. Set it to the specific origin making the request (e.g., 'https://myapp.com') or '*' for public APIs. The fix must be on the server side — you cannot fix CORS errors from the frontend.
What is the difference between simple and preflight requests?
Simple requests use GET, HEAD, or POST with standard headers and form content types — the browser sends them directly. Non-simple requests (PUT, DELETE, custom headers, JSON content type) trigger a preflight OPTIONS request first to check if the server allows them.
Can I fix CORS from the frontend?
No. CORS is enforced by the browser but configured on the server. The server must include the correct Access-Control-Allow-Origin header in its response. Frontend code cannot bypass CORS — that's the entire point of the security mechanism.
Should I use Access-Control-Allow-Origin: * in production?
Only for truly public APIs that don't use cookies or authentication. The wildcard '*' allows any website to read your responses. For APIs with authentication, always specify exact origins. Note: '*' cannot be used with credentials (cookies) — the browser will reject it.
What is Access-Control-Max-Age?
Access-Control-Max-Age tells the browser how long (in seconds) to cache the preflight response. During this time, the browser skips the preflight OPTIONS request for identical cross-origin requests. Set to 86400 (24 hours) for stable APIs to reduce latency.
Why does my CORS work in Postman but not the browser?
Postman is not a browser — it doesn't enforce CORS. CORS is a browser security feature that restricts JavaScript from reading cross-origin responses. Tools like Postman, curl, and server-to-server requests bypass CORS entirely because they don't have the same security context.
What is the difference between CORS, CSRF, and CSP?
CORS controls which origins can read your API responses (browser-enforced). CSRF (Cross-Site Request Forgery) is an attack where a malicious site tricks a user's browser into making unwanted requests. CSP (Content Security Policy) controls which resources a page can load. They are three separate security mechanisms solving different problems.

Related Articles & Tools

Conclusion

CORS feels like the browser is fighting you, but it's actually protecting your users. The Same-Origin Policy prevents malicious sites from stealing data, and CORS is the controlled exception that lets legitimate cross-origin communication happen.

The mental model is simple: your server tells the browser who's allowed to read responses. If the requesting origin isn't on the list, the browser blocks the read. Preflight requests are just the browser asking permission before sending anything "unusual."

Once you internalize the flow — Origin check → preflight (if non-simple) → response headers → browser decision — you'll fix CORS issues in seconds instead of hours. And you'll stop reaching for Access-Control-Allow-Origin: * as a band-aid.

Set specific origins, handle OPTIONS, cache your preflights, and test in the actual browser. That's 90% of CORS done right.