CORS (Cross-Origin Resource Sharing)

How servers opt in to allowing cross-origin requests from browsers

The Same-Origin Policy

Browsers enforce the Same-Origin Policy: JavaScript running on one origin cannot read responses from a different origin. Two URLs have the same origin only if they share the same scheme, host, and port.

URL A URL B Same Origin? Why
https://example.com/a https://example.com/b Yes Same scheme, host, port
https://example.com http://example.com No Different scheme (https vs http)
https://example.com https://api.example.com No Different host (subdomain counts)
https://example.com https://example.com:8080 No Different port

CORS is the mechanism that lets a server opt in to allowing cross-origin requests. The server sends specific headers that tell the browser: "requests from this origin are allowed."

Simple Requests vs Preflight Requests

Browsers classify cross-origin requests into two categories:

Simple requests (no preflight) — must meet all of these:

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

Preflighted requests — anything else (PUT, DELETE, custom headers, application/json, etc.). The browser sends an OPTIONS request first to ask the server for permission.

Simple Request (GET with safe headers):

Browser (app.com)                              Server (api.other.com)
   │                                              │
   │  GET /data HTTP/1.1                          │
   │  Origin: https://app.com ───────────────────▶│
   │                                              │
   │  HTTP/1.1 200 OK                             │
   │  Access-Control-Allow-Origin: https://app.com│
   │  [data] ◀────────────────────────────────────│
   │                                              │

Preflighted Request (PUT with JSON):

Browser (app.com)                              Server (api.other.com)
   │                                              │
   │  1. Preflight (automatic)                    │
   │  OPTIONS /data HTTP/1.1                      │
   │  Origin: https://app.com                     │
   │  Access-Control-Request-Method: PUT          │
   │  Access-Control-Request-Headers: Content-Type│
   │ ────────────────────────────────────────────▶ │
   │                                              │
   │  HTTP/1.1 204 No Content                     │
   │  Access-Control-Allow-Origin: https://app.com│
   │  Access-Control-Allow-Methods: GET, PUT, POST│
   │  Access-Control-Allow-Headers: Content-Type  │
   │  Access-Control-Max-Age: 86400               │
   │ ◀──────────────────────────────────────────── │
   │                                              │
   │  2. Actual request (only if preflight passes)│
   │  PUT /data HTTP/1.1                          │
   │  Origin: https://app.com                     │
   │  Content-Type: application/json              │
   │  {"key": "value"} ─────────────────────────▶ │
   │                                              │
   │  HTTP/1.1 200 OK                             │
   │  Access-Control-Allow-Origin: https://app.com│
   │  [response] ◀───────────────────────────────│
   │                                              │

CORS Headers

Header Direction Purpose
Origin Request The origin making the request
Access-Control-Allow-Origin Response Which origins are allowed (* = any, or a specific origin)
Access-Control-Allow-Methods Response (preflight) Which HTTP methods are allowed
Access-Control-Allow-Headers Response (preflight) Which request headers are allowed
Access-Control-Allow-Credentials Response Whether cookies/auth headers are allowed (true)
Access-Control-Max-Age Response (preflight) How long (seconds) to cache the preflight result