The same-origin policy

By default, scripts running on one origin (scheme + host + port) cannot read responses from a different origin. This blanket restriction prevents a malicious page from reading your bank balance via the cookies the browser would otherwise attach. CORS is the controlled escape valve.

Simple requests

A "simple" request — GET, HEAD, or a POST with only safelisted headers and a body of one of three media types (application/x-www-form-urlencoded, multipart/form-data, or text/plain) — is sent without a preflight. The browser includes an Origin header; the server must respond with Access-Control-Allow-Origin or the browser blocks the response from being read by the script.

Request:
GET /api/data HTTP/1.1
Origin: https://app.example.com

Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin

Preflighted requests

If a request uses a method other than GET/HEAD/POST, custom headers, or a non-simple content type, the browser first sends an OPTIONS preflight to check that the server allows it. Only if the preflight succeeds does the actual request go out.

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

Preflight response:
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: Authorization, Content-Type
Access-Control-Max-Age: 86400
!

Credentials and wildcards don't mix. If Access-Control-Allow-Credentials: true, the browser refuses Access-Control-Allow-Origin: *. You must echo the specific origin instead — and add Vary: Origin so caches don't mix responses.

Common pitfalls