Lesson 17 · FE Lead Interview · 2026-06-12
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 = protocol + host + port. All three must match for same-origin. https://example.com ≠ https://api.example.com ≠ http://example.com.
| Simple (no preflight) | Preflighted |
|---|---|
| GET / HEAD / POST | PUT / DELETE / PATCH / … |
| Only safelisted headers | Any custom header (Authorization, X-*) |
| Content-Type: text/plain, multipart/form-data, application/x-www-form-urlencoded | Content-Type: application/json ← |
| Header | Value |
|---|---|
Access-Control-Allow-Origin | Explicit origin or * (not both) |
Access-Control-Allow-Methods | Comma list: POST, GET, OPTIONS |
Access-Control-Allow-Headers | Echo back requested headers |
Access-Control-Allow-Credentials | true if sending cookies |
Access-Control-Max-Age | Seconds to cache preflight (Chrome max 7200) |
Access-Control-Expose-Headers | Non-safelisted headers JS may read |
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
fetch(url, { credentials: 'include' });
// server MUST return:
// Access-Control-Allow-Origin: https://example.com ← no *
// Access-Control-Allow-Credentials: true
Vary: OriginDynamic reflection = must Vary: Origin.
Wildcard * = response identical for all origins = no Vary needed (but no credentials either).
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');
});
Your JSON API always preflights — application/json is not on the simple Content-Type safelist.
Spec forbids * when credentials mode is 'include'.
Fix: set explicit origin and Access-Control-Allow-Credentials: true.
If auth check runs before CORS middleware, 401s have no CORS headers → browser shows CORS error, not 401. CORS middleware must run first.
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.
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).