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.com | Same origin? | Why |
|---|---|---|
https://app.example.com/other | yes | path is not part of the origin |
http://app.example.com | no | different scheme |
https://app.example.com:8443 | no | different port |
https://api.example.com | no | different host |
https://example.com | no | subdomain 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
| Header | Value | Notes |
|---|---|---|
Access-Control-Allow-Origin | one origin, or * | no lists, no wildcards inside a domain |
Access-Control-Allow-Credentials | true | any other value is treated as false |
Access-Control-Allow-Methods | GET, POST, PATCH | preflight response only, * allowed without credentials |
Access-Control-Allow-Headers | authorization, content-type | preflight only, must list every non-safelisted header |
Access-Control-Expose-Headers | etag, x-request-id | which response headers JS may read |
Access-Control-Max-Age | seconds | preflight cache, capped at 7200 in Chrome, 86400 in Firefox |
Vary: Origin | — | required whenever you echo the origin, or caches cross-contaminate |
Timing-Allow-Origin | origin 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:
| Header | With credentials |
|---|---|
Access-Control-Allow-Origin | must be the exact origin, * is rejected |
Access-Control-Allow-Credentials | must be true |
Access-Control-Allow-Headers | must list headers explicitly, * does not count |
Access-Control-Allow-Methods | must 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 message | Real cause |
|---|---|
No 'Access-Control-Allow-Origin' header is present | server 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 allowed | missing from Access-Control-Allow-Headers |
Method PATCH is not allowed | missing from Access-Control-Allow-Methods |
Redirect is not allowed for a preflight request | the OPTIONS route 301s, commonly a trailing-slash or HTTP-to-HTTPS redirect |
| Response arrives but a header is unreadable | needs Access-Control-Expose-Headers |
Works in curl, fails in the browser | as 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.