CORS — Cheat Sheet

Lesson 17 · FE Lead Interview · 2026-06-12

Core mental model

Memorize

CORS = browser contract, not server lock. curl ignores it. Your server is reachable regardless — CORS only controls whether browser JS can read the response.

Origin formula

Origin = protocol + host + port. All three must match for same-origin. https://example.comhttps://api.example.comhttp://example.com.

Simple vs Preflight

Simple (no preflight)Preflighted
GET / HEAD / POSTPUT / DELETE / PATCH / …
Only safelisted headersAny custom header (Authorization, X-*)
Content-Type: text/plain, multipart/form-data, application/x-www-form-urlencodedContent-Type: application/json ←

Response headers (server must set)

HeaderValue
Access-Control-Allow-OriginExplicit origin or * (not both)
Access-Control-Allow-MethodsComma list: POST, GET, OPTIONS
Access-Control-Allow-HeadersEcho back requested headers
Access-Control-Allow-Credentialstrue if sending cookies
Access-Control-Max-AgeSeconds to cache preflight (Chrome max 7200)
Access-Control-Expose-HeadersNon-safelisted headers JS may read

Preflight flow

OPTIONS /api/hotels
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

→ 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

→ actual POST fires

Credentials

fetch(url, { credentials: 'include' });
// server MUST return:
// Access-Control-Allow-Origin: https://example.com  ← no *
// Access-Control-Allow-Credentials: true

Debug checklist

Step by step
  1. Read the exact Chrome error message
  2. Find the OPTIONS preflight in Network tab
  3. Check ACAO header on the blocked request
  4. Check credentials + wildcard combo
  5. Check CORS headers on error responses too
  6. If CDN: check Vary: Origin

Dynamic reflection & Vary: Origin

Memorize

Dynamic reflection = must Vary: Origin.
Wildcard * = response identical for all origins = no Vary needed (but no credentials either).

WebSocket ≠ CORS

Memorize

fetch: browser enforces CORS — server protected by default.
WebSocket: browser sends Origin, enforces nothing — server must check.

// Server-side defence (ws library)
wss.on('connection', (socket, req) => {
  if (!ALLOWED.has(req.headers.origin))
    socket.close(1008, 'Origin not allowed');
});

What CORS doesn't do

Don't fail the interview

application/json is NOT simple

Your JSON API always preflights — application/json is not on the simple Content-Type safelist.

Wildcard * + credentials = always broken

Spec forbids * when credentials mode is 'include'.
Fix: set explicit origin and Access-Control-Allow-Credentials: true.

CORS middleware order

If auth check runs before CORS middleware, 401s have no CORS headers → browser shows CORS error, not 401. CORS middleware must run first.

CDN Vary: Origin — the production time bomb

Dynamic reflection without Vary: Origin: CDN keys by URL. Origin A's cached headers served to Origin B → CORS error. Also: a no-Origin server request warms the cache with no ACAO → first browser request is a CDN hit with no ACAO → CORS error.

Fix: always add Vary: Origin alongside reflected ACAO. On CloudFront: Vary alone is insufficient — must add Origin to the cache policy → Headers explicitly.

WebSocket has no browser safety net

Browser sends Origin on upgrade but never checks ACAO in the 101 response. No preflight. The server must validate Origin itself — or any site can open an authenticated socket with the victim's cookies (WebSocket has no credentials option; cookies always send).