Skip to content

CORS

If your API is called from browser JavaScript on a different origin, you will meet CORS. It is one of the most misunderstood parts of web APIs, so let’s be precise about what it is and is not.

The browser’s same-origin policy blocks a page on https://app.example.com from reading a response from https://api.example.com unless the API explicitly opts in with Cross-Origin Resource Sharing headers. CORS is enforced by the browser, on behalf of the user — it is not a server-side access control.

For “non-simple” requests (most JSON POST/PUT/DELETE, custom headers), the browser first sends a preflight OPTIONS request asking permission:

sequenceDiagram
  participant B as Browser
  participant S as API
  B->>S: OPTIONS /articles (preflight)
  S-->>B: 204 + Access-Control-Allow-Origin/Methods/Headers
  B->>S: POST /articles (actual request)
  S-->>B: 201 Created + Access-Control-Allow-Origin
The browser checks permission with OPTIONS before the real request

The key response headers: Access-Control-Allow-Origin (which origins may read responses), -Allow-Methods, -Allow-Headers, and -Allow-Credentials (whether cookies/credentials are allowed).

JavaScript

Avoid the classic mistake of reflecting any origin (Access-Control-Allow-Origin: *) together with Allow-Credentials: true — the combination is forbidden and, more importantly, would expose authenticated responses to any site.

Access-Control-Allow-Origin: * with credentials Symptom:

  • Setting Access-Control-Allow-Origin: * while also sending cookies or an Authorization header
  • The browser blocks the request — * doesn’t work with credentials
  • Specify the real origin, e.g. https://app.example.com, whenever credentials are used

A preflight on every request because nothing gets cached Symptom:

  • No Access-Control-Max-Age set — the browser sends an OPTIONS preflight every time
  • Extra latency on every request
  • Set Access-Control-Max-Age: 86400 to cache the preflight result for a day

💡 Real-world examples

GitHub API:

  • CORS allows only origins registered through an OAuth App
  • No * for authenticated endpoints

Stripe.js:

  • The browser SDK calls the Stripe API directly — CORS is scoped to Stripe’s own domains
  • The API key used in the browser is a publishable key only (no charge permission)

Sources: GitHub OAuth Apps, Stripe.js and Elements

Who enforces CORS?
What is the preflight request?
Why is `Allow-Origin: *` with `Allow-Credentials: true` a problem?