HTTP Cheatsheet

CORS

Use this HTTP reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

The rule CORS relaxes

The same-origin policy blocks JavaScript from reading a response from a different origin. An origin is the triple scheme + host + port, and all three must match.

Compared with https://app.example.comSame origin?Why
https://app.example.com/otheryespath is not part of the origin
http://app.example.comnodifferent scheme
https://app.example.com:8443nodifferent port
https://api.example.comnodifferent host
https://example.comnosubdomain counts as a different host

CORS is a browser mechanism. The request usually still reaches the server and still has effects, and the browser only withholds the response from the script. It is not an authorization layer, so never rely on it to protect data. curl and server-side code ignore CORS entirely.

Simple vs preflighted requests

A request skips the preflight only if it meets every condition:

1. Method is GET, HEAD, or POST. 2. Headers are limited to CORS-safelisted ones: Accept, Accept-Language, Content-Language, Content-Type, Range (simple values only). 3. Content-Type is one of application/x-www-form-urlencoded, multipart/form-data, or text/plain. 4. No upload progress listener is attached to the XMLHttpRequest. 5. No ReadableStream body.

Anything else preflights. In practice Content-Type: application/json preflights, and so does any Authorization or custom header, which is why almost every real API call triggers one.

The preflight sequence

1. Browser sends the preflight, automatically
   OPTIONS /v1/orders HTTP/1.1
   Host: api.example.com
   Origin: https://app.example.com
   Access-Control-Request-Method: PATCH
   Access-Control-Request-Headers: authorization, content-type

2. Server answers, no body needed
   HTTP/1.1 204 No Content
   Access-Control-Allow-Origin: https://app.example.com
   Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
   Access-Control-Allow-Headers: authorization, content-type
   Access-Control-Allow-Credentials: true
   Access-Control-Max-Age: 86400
   Vary: Origin

3. Only now does the real request go out
   PATCH /v1/orders HTTP/1.1
   Origin: https://app.example.com
   Authorization: Bearer ...
   Content-Type: application/json

4. And the real response must repeat the origin headers
   HTTP/1.1 200 OK
   Access-Control-Allow-Origin: https://app.example.com
   Access-Control-Allow-Credentials: true
   Access-Control-Expose-Headers: etag, x-request-id
   Vary: Origin

Step 4 is the one people forget. Passing the preflight is not enough, the actual response needs the headers too or the browser still blocks it.

Response header reference

HeaderValueNotes
Access-Control-Allow-Originone origin, or *no lists, no wildcards inside a domain
Access-Control-Allow-Credentialstrueany other value is treated as false
Access-Control-Allow-MethodsGET, POST, PATCHpreflight response only, * allowed without credentials
Access-Control-Allow-Headersauthorization, content-typepreflight only, must list every non-safelisted header
Access-Control-Expose-Headersetag, x-request-idwhich response headers JS may read
Access-Control-Max-Agesecondspreflight cache, capped at 7200 in Chrome, 86400 in Firefox
Vary: Origin—required whenever you echo the origin, or caches cross-contaminate
Timing-Allow-Originorigin or *exposes Resource Timing detail

Without Access-Control-Expose-Headers, JavaScript can only read a short safelist: Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma. Everything else, including your ETag and pagination headers, is invisible until you expose it.

Credentials, and why * stops working

When the request carries credentials (cookies, HTTP auth, or client certs), the rules tighten:

HeaderWith credentials
Access-Control-Allow-Originmust be the exact origin, * is rejected
Access-Control-Allow-Credentialsmust be true
Access-Control-Allow-Headersmust list headers explicitly, * does not count
Access-Control-Allow-Methodsmust list methods explicitly
// the client has to opt in as well
await fetch('https://api.example.com/v1/me', {
  credentials: 'include',        // 'same-origin' is the default
  headers: { 'content-type': 'application/json' },
})

Cookies additionally need SameSite=None; Secure to survive a cross-site request, so a CORS setup can be perfect and still lose the session to the cookie layer.

Server configuration

// Express, explicit allowlist rather than a reflect-anything wildcard
const ALLOWED = new Set([
  'https://app.example.com',
  'https://staging.example.com',
])

app.use((req, res, next) => {
  const origin = req.headers.origin
  if (origin && ALLOWED.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin)
    res.setHeader('Access-Control-Allow-Credentials', 'true')
    res.setHeader('Vary', 'Origin')
  }
  if (req.method === 'OPTIONS') {
    res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PATCH,DELETE')
    res.setHeader('Access-Control-Allow-Headers', 'authorization,content-type')
    res.setHeader('Access-Control-Max-Age', '86400')
    return res.status(204).end()
  }
  next()
})
# nginx
location /v1/ {
  if ($request_method = OPTIONS) {
    add_header Access-Control-Allow-Origin  $http_origin always;
    add_header Access-Control-Allow-Methods "GET,POST,PATCH,DELETE" always;
    add_header Access-Control-Allow-Headers "authorization,content-type" always;
    add_header Access-Control-Max-Age       86400 always;
    add_header Vary Origin always;
    return 204;
  }
  add_header Access-Control-Allow-Origin $http_origin always;
  add_header Vary Origin always;
  proxy_pass http://app;
}

Never reflect Origin unconditionally while allowing credentials. That is equivalent to allowing every site on the internet to make authenticated requests as your users and read the results.

Testing and diagnosing

# simulate the preflight
curl -i -X OPTIONS https://api.example.com/v1/orders \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: PATCH' \
  -H 'Access-Control-Request-Headers: authorization, content-type'

# check the real response carries the headers too
curl -i https://api.example.com/v1/orders \
  -H 'Origin: https://app.example.com' | grep -i access-control

# confirm a rejected origin is actually rejected
curl -sI https://api.example.com/v1/orders \
  -H 'Origin: https://evil.example' | grep -i access-control
Browser console messageReal cause
No 'Access-Control-Allow-Origin' header is presentserver sent nothing, often because it 500'd or the route does not handle OPTIONS
The value of the 'Access-Control-Allow-Origin' header ... must not be the wildcard* used together with credentials
Request header field authorization is not allowedmissing from Access-Control-Allow-Headers
Method PATCH is not allowedmissing from Access-Control-Allow-Methods
Redirect is not allowed for a preflight requestthe OPTIONS route 301s, commonly a trailing-slash or HTTP-to-HTTPS redirect
Response arrives but a header is unreadableneeds Access-Control-Expose-Headers
Works in curl, fails in the browseras designed, curl does not enforce CORS

Two structural alternatives worth knowing. A same-origin reverse proxy (serve the API under /api on the app's own origin) removes CORS from the picture entirely, and it is usually the simplest production answer. JSONP is the pre-CORS hack, it only does GET, and it is a script-injection risk, so do not introduce it.