CORS Explained

Same-origin policy, simple vs preflighted requests, the OPTIONS preflight, Access-Control-* headers, credentials, and the errors you'll actually hit.

CORS (Cross-Origin Resource Sharing) is the standard mechanism by which a server explicitly permits JavaScript on one origin to read resources from another. To understand it you first need the Same-Origin Policy.

The same-origin policy

An origin is the combination of scheme + host + port. https://app.example.com and https://api.example.com differ in host, so they're different origins; http:// vs https:// differ, and a different port differs too. For security, browsers block a script from reading a response whose origin differs from the script's own. That's the same-origin policy, and it's a core defense against attacks that abuse the user's cookies.

CORS doesn't punch a hole in that policy so much as let the server declare, via response headers, that a given origin is allowed — opening cross-origin communication safely. Crucial point: CORS is enforced by the browser. It does not apply to curl or server-to-server calls.

Simple vs preflighted requests

The browser splits cross-origin requests into two kinds. A simple request uses GET/HEAD/POST with only a safe, restricted set of headers (for example a Content-Type of text/plain, application/x-www-form-urlencoded, or multipart/form-data). The browser sends it immediately, then checks the response's Access-Control-Allow-Origin header to decide whether the script may read it.

Anything else — say, Content-Type: application/json, a PUT/DELETE, or custom headers — triggers a preflight. Before sending the real request, the browser first sends an OPTIONS request asking the server 'am I allowed to send this?'

OPTIONS /v1/users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization

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

If the server answers the preflight with the right allow headers, the browser then sends the actual PUT. Access-Control-Max-Age tells the browser how long to cache that permission, avoiding a repeated preflight on every request.

Credentials

To include credentials such as cookies or an Authorization header on a cross-origin request, both sides must opt in. The client requests with fetch(url, { credentials: 'include' }), and the server must return Access-Control-Allow-Credentials: true. A key constraint: when credentials are allowed, Access-Control-Allow-Origin cannot be the wildcard * — it must name a specific origin.

Errors you'll actually hit