1. Library
  2. Computer Networks
  3. Http and the Web
  4. Headers

Updated 9 hours ago

You make a fetch request. The server responds. And then your browser throws it away.

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.

This is CORS—Cross-Origin Resource Sharing. And here's the thing that transforms frustration into understanding: the server already processed your request. It ran your code, touched your database, returned your data. CORS just decides whether your JavaScript gets to see what happened.

CORS is the browser playing bouncer, checking the guest list on every response.

Why Browsers Do This

Imagine you're logged into your bank at https://mybank.com. Your browser holds your session cookie. Now you visit https://sketchy-site.com, and hidden JavaScript runs:

fetch('https://mybank.com/api/transfer', {
    method: 'POST',
    credentials: 'include',
    body: JSON.stringify({ to: 'attacker', amount: 10000 })
});

Without CORS, this works. Your browser dutifully sends your bank's cookies with the request. The transfer executes. The attacker gets your money.

CORS prevents this by requiring https://mybank.com to explicitly say "yes, I trust requests from https://sketchy-site.com." Since no legitimate bank would say that, the browser blocks the response—and more importantly, blocks credentialed requests entirely without prior permission.

What Makes an Origin

An origin is the combination of scheme, hostname, and port:

  • https://example.com and http://example.com — different origins (different scheme)
  • https://example.com and https://api.example.com — different origins (different hostname)
  • https://example.com:443 and https://example.com:8443 — different origins (different port)

Same origin means all three match exactly.

The Headers That Control Everything

Access-Control-Allow-Origin

The fundamental permission slip:

Access-Control-Allow-Origin: https://app.example.com

This says: "JavaScript from https://app.example.com may access this response." Any other origin gets blocked.

The wildcard allows everyone:

Access-Control-Allow-Origin: *

Fine for truly public data. Dangerous for anything else. And crucially: wildcards don't work with credentials.

Access-Control-Allow-Credentials

By default, cross-origin requests don't include cookies or Authorization headers. To enable them:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

Both are required. And notice: no wildcard. When credentials are involved, the server must name the specific origin it trusts. This is intentional—you can't say "I trust everyone with my users' authentication."

Access-Control-Allow-Methods

Which HTTP methods are permitted:

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

Access-Control-Allow-Headers

Which request headers are permitted:

Access-Control-Allow-Headers: Content-Type, Authorization

By default, only a handful of "safe" headers are allowed. The moment you add Authorization or Content-Type: application/json, you need explicit permission.

Access-Control-Max-Age

How long (in seconds) to cache the permission check:

Access-Control-Max-Age: 86400

This matters because of preflight requests.

Access-Control-Expose-Headers

By default, JavaScript can only see a few response headers. To expose custom headers:

Access-Control-Expose-Headers: X-Request-ID, X-RateLimit-Remaining

Simple Requests vs. Preflight

Some requests are "simple"—browsers send them directly and check CORS on the response:

  • GET, HEAD, or POST method
  • Only standard headers (Accept, Content-Type with form values, etc.)
  • Content-Type limited to application/x-www-form-urlencoded, multipart/form-data, or text/plain

Everything else triggers a preflight—an OPTIONS request that asks permission before the real request:

OPTIONS /api/data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization

The server responds with what it allows:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400

If the preflight passes, the browser sends the actual request. If it fails, the real request never happens.

This is why Access-Control-Max-Age matters—without caching, every API call with custom headers requires two round trips.

Common Errors and What They Mean

"No 'Access-Control-Allow-Origin' header is present"

The server didn't include CORS headers at all. Either CORS isn't configured, or it's not configured for your origin.

"The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when the request's credentials mode is 'include'"

You're sending credentials (cookies, Authorization) but the server returned *. It must return your exact origin.

"Method PUT is not allowed by Access-Control-Allow-Methods"

The preflight response didn't include PUT. Add it to the allowed methods.

"Request header field authorization is not allowed by Access-Control-Allow-Headers"

The preflight response didn't permit the Authorization header. Add it.

Debugging

Open DevTools. Two tabs matter:

Console: Shows the CORS error with details about what failed.

Network: Shows the actual requests. Look for:

  • The OPTIONS preflight (if present)
  • The response headers (or their absence)
  • Whether the request was blocked vs. the response was blocked

Remember: if you see the request in the Network tab with a response, the server processed it. CORS just prevented your JavaScript from reading the result.

Server Configuration

Express/Node.js:

const cors = require('cors');
app.use(cors({
    origin: 'https://app.example.com',
    credentials: true
}));

Nginx:

location /api {
    add_header Access-Control-Allow-Origin https://app.example.com always;
    add_header Access-Control-Allow-Credentials true always;
    add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE' always;
    add_header Access-Control-Allow-Headers 'Content-Type, Authorization' always;
    
    if ($request_method = OPTIONS) {
        return 204;
    }
}

The always directive ensures headers appear even on error responses—otherwise a 500 error might lack CORS headers, making debugging harder.

Security Realities

CORS is browser security, not server security. Anyone with curl, Postman, or a custom client bypasses CORS entirely. It protects users from malicious websites, not your API from malicious actors.

Never echo the Origin header blindly:

// DANGEROUS - allows any origin
response.header('Access-Control-Allow-Origin', request.headers.origin);

Maintain an allowlist:

const allowed = ['https://app.example.com', 'https://admin.example.com'];
const origin = request.headers.origin;
if (allowed.includes(origin)) {
    response.header('Access-Control-Allow-Origin', origin);
}

Credentials require extra care. Access-Control-Allow-Credentials: true means "this origin can make authenticated requests as the user." Only grant this to origins you fully control.

Wildcards are for public data only. If your API requires authentication, * is wrong.

The Mental Model

CORS is a conversation between your browser and the server:

  1. Browser: "I'm JavaScript from origin X, wanting to access origin Y"
  2. Server: "Here's my response, and here's who I trust" (via headers)
  3. Browser: "Is origin X on that list? If yes, JavaScript gets the data. If no, blocked."

The server always processes the request. CORS only controls whether the browser shares the response with JavaScript.

This is why CORS errors feel backwards—you can see in DevTools that the server responded successfully, yet your code can't access it. The response exists. The browser just won't let you have it.

Frequently Asked Questions About CORS

Was this page helpful?

😔
🤨
😃
CORS Headers • Library • Connected