CORS Deep Dive

Cross-Origin Resource Sharing (CORS) is the browser mechanism that controls when JavaScript on one origin can access resources on another. It's confusing because the protections are mostly browser-imposed (servers usually allow everything; browsers block).

This page is about how CORS actually works and the configuration patterns.

The same-origin policy

By default, browsers prevent JavaScript on https://app.example.com from making requests to https://api.example.com. The "origin" is scheme + host + port.

The protection is for users, not servers. The point: a malicious page can't read your bank's data even if you're logged in.

Why CORS exists

Without CORS, every cross-origin XHR/fetch would be blocked. Modern web apps make many cross-origin requests legitimately (calling APIs, embedding fonts, loading images). CORS is the mechanism for the server to opt into allowing specific cross-origin requests.

How CORS works

Simple requests

GET, HEAD, or POST with simple content types (form-data, text/plain) and no special headers can be sent immediately. The browser includes:

Origin: https://app.example.com

The server responds with:

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

(Or * for any origin, with limitations.)

If the response has the right header, the browser lets JS read it. If not, the browser blocks JS access (the request was sent and the response received, but JS can't read it).

Preflight requests

For requests that aren't simple (DELETE, PUT, custom headers, JSON content type), the browser sends an OPTIONS preflight request first:

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

Server responds:

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, Content-Type
Access-Control-Max-Age: 3600

The browser then sends the actual request.

The preflight is cached per the Max-Age; subsequent requests skip it.

The major headers

Access-Control-Allow-Origin

The origin that's allowed. Either a specific origin or * (any).

* doesn't work with credentials (cookies, auth headers). For authenticated cross-origin requests, you must specify the origin.

Access-Control-Allow-Credentials

If true, cookies and auth headers can be sent. Requires specific Allow-Origin (not *).

Access-Control-Allow-Methods

Methods the resource supports. For preflight responses.

Access-Control-Allow-Headers

Headers the request can include. For preflight responses.

Access-Control-Expose-Headers

Headers JS can read. By default, only a subset of response headers are visible to JS; this header expands the set.

Access-Control-Max-Age

How long preflight result is cached. Longer means fewer preflights.

Patterns that work

API gateway with explicit allowed origins

Allow-Origin: comes from a list — production-app.example.com, staging.example.com, localhost:3000 for dev

Each request's Origin header is checked against the list; matching origin is echoed back. Not in list: no header, browser blocks.

This is the right pattern for production APIs.

Wildcard for public resources

Access-Control-Allow-Origin: *

For genuinely public APIs (read-only, no credentials needed), wildcard is fine.

Subdomain handling

If you control multiple subdomains:

*.example.com → all subdomains allowed

Implemented by checking the Origin header; not by literal wildcard in the header value (browsers don't support wildcards in Allow-Origin beyond *).

Common configuration errors

Returning multiple Allow-Origin headers

Browsers reject. Specify one origin (echoed from request) or *.

Wildcard with credentials

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Browser rejects. Use specific origin.

Forgetting OPTIONS

The preflight needs a 200/204 response. If the API returns 401 for OPTIONS (because not authenticated), CORS fails.

Configure the framework to short-circuit OPTIONS to a CORS-aware handler before auth.

Cache headers ignored

Different origins might have different CORS rules. If responses are cached, the browser might return the wrong CORS headers. Use Vary: Origin to inform caches.

CORS isn't security

A common misunderstanding: CORS protects the server. It doesn't.

CORS protects users from malicious scripts in their browser reading data they shouldn't. The server still receives every request; the browser is what blocks JS access to responses.

Server-side authentication and authorization are still required. CORS is in addition to, not instead of.

CORS in different scenarios

Browser → public API

Standard CORS. Origin specific or wildcard.

Browser → authenticated API

Specific origin; allow-credentials true; cookies or auth headers.

Server → server

CORS doesn't apply. Servers don't enforce same-origin policy. CORS is browser-only.

Mobile apps → API

Mobile apps don't have a browser; CORS doesn't apply. The app's HTTP client just makes the request.

CLI tools → API

Same — no CORS.

Common failure patterns

Further Reading