How does CORS work and when does the browser send a preflight?
Take a moment.
Say your answer out loud or write it down first — retrieval is what makes it stick. Reveal when you're ready.
Short answer
CORS is the browser's way of asking a server whether cross-origin JavaScript may read its responses. Simple GET/POST requests go through and the browser checks Access-Control-Allow-Origin on the way back; riskier requests (other methods, custom headers, JSON bodies) first send an OPTIONS preflight asking permission. Servers opt in with ACAO/ACAM/ACAH headers — CORS never protects the server, only the user's data from other origins.
Detailed explanation
The same-origin policy blocks page scripts from reading responses cross-origin by default — without it, any site you visit could fetch your webmail. CORS is the controlled exception. Simple requests (GET/HEAD/POST with form-compatible content types and no custom headers) are sent immediately; the browser then permits your script to read the response only if it carries Access-Control-Allow-Origin matching your origin (or * for public data — never * with credentials). Everything else triggers a preflight: an OPTIONS request describing the real one (Access-Control-Request-Method: PUT, Access-Control-Request-Headers: X-Custom), to which the server replies with what it allows (Allow-Methods, Allow-Headers, Max-Age to cache the verdict). Only then does the browser send the real request. Credentials mode is the sharp edge: fetch with credentials: 'include' sends cookies cross-origin, and the server must echo the exact origin (no *) plus Access-Control-Allow-Credentials: true. Key mental model: CORS headers don't block attackers from reaching your server — curl ignores them entirely. They stop the victim's browser from handing another origin's data to malicious script. So server auth and rate limits still apply; misconfigurations like reflecting any Origin with credentials enabled punch a hole through the whole model.
Example
// App at app.example.com calling api.example.com
// PUT + custom header → preflight first:
// OPTIONS /profile
// Origin: https://app.example.com
// Access-Control-Request-Method: PUT
// Access-Control-Request-Headers: X-Csrf-Token
//
// ← 204 No Content
// Access-Control-Allow-Origin: https://app.example.com
// Access-Control-Allow-Methods: PUT
// Access-Control-Allow-Headers: X-Csrf-Token
// Access-Control-Max-Age: 86400
// Then the real PUT goes out.
// Public read-only data: simplest valid config
// Access-Control-Allow-Origin: *Interview tip
State the mental model first (CORS protects users' data from other origins, not your server from attackers), then describe the preflight trigger and the credentials sharp edge. That ordering answers most follow-ups before they're asked.
Common mistakes
- Echoing any request Origin with Allow-Credentials — handing data to everyone.
- Using ACAO: * together with credentials, which browsers reject.
- Thinking CORS errors mean the server is broken — the request often succeeded, the browser just hid the response.