4.3 KiB
4.3 KiB
CORS classroom
A live playground for teaching CORS. One zero-dependency Node file starts three origins; the page fires real cross-origin requests and shows the browser's verdict next to the server's own log of what it actually received.
node server.js # or: npm start
open http://localhost:3000
Then, in this order:
- Open DevTools → Network and leave it visible. Every "blocked" response is still there, which is the point of the first scenario.
- Click Set cookies on API once, so the credentials scenarios have cookies to send.
- Work down the six groups, or hit Run all and talk through the results.
What's running
| Origin | Role |
|---|---|
http://localhost:3000 |
the page. Also serves /same-origin/api and the log stream — both same-origin, so neither is affected by CORS. |
http://localhost:3001 |
the API. A different origin from the page, but the same site, so cookies still travel there. |
http://127.0.0.1:3001 |
the same listener reached as a different site, which is how the SameSite demo works. |
The API builds its CORS headers from query parameters on every request, so each scenario reconfigures the server as it runs. Nothing needs restarting, and you can hand-edit any URL live in front of the class.
| Param | Effect |
|---|---|
acao=* / echo / literal |
Access-Control-Allow-Origin (omit the param to send no header at all) |
acac=true |
Access-Control-Allow-Credentials |
acam= / acah= |
Allow-Methods / Allow-Headers; echo reflects what the preflight asked for |
aceh= |
Access-Control-Expose-Headers |
acma=60 |
Access-Control-Max-Age (preflight cache) |
vary=1 |
adds Vary: Origin |
pfstatus=401 |
status code for the OPTIONS preflight |
pfcors=0 |
answer the preflight with no CORS headers |
pfacao=0 / pfacac=0 |
omit that header on the preflight only |
pfredirect=1 |
make the preflight respond 302 |
status=500 |
status of the actual response |
errnocors=1 |
drop CORS headers when status ≥ 400 — the classic middleware-order bug |
delay=400 |
artificial latency, to show what a preflight costs |
The six groups
- The basic rule — CORS only applies cross-origin, and it gates reading the
response, not sending the request. Includes the ways
Allow-Originis commonly written wrong: a comma-separated list, a trailing slash, unconditional reflection. - Preflight mechanics — what makes a request simple, what triggers OPTIONS,
why a failed preflight means the real request never reaches your server, and
Max-Age. TheMax-Agecard is worth running twice: the second run has no OPTIONS. - Credentials and wildcards — the
*-with-credentials rule,Allow-Credentialson the preflight as well as the response, and a cross-site scenario where CORS passes but the cookie still isn't sent, to separate CORS from SameSite. - Reading the response — a successful request still hides every response header
outside the seven-header safelist, and
Expose-Headers: *silently stops working once credentials are involved. - How this breaks in real life — a 500 that reports itself as a CORS error, auth
middleware answering the preflight with 401,
mode: 'no-cors'and what it really does, and a cross-origin form POST that succeeds — the CORS-is-not-CSRF-protection point. - Free playground — every knob as a form, with both preview panels updating as you type, for when someone asks "but what if…".
Files
server.js— the whole backend, ~330 lines of plain Node, written to be read out loud.public/scenarios.js— the scenario catalogue as data: config, expectation, and the teaching note for each. Add your own here.public/app.js— the runner: predicts whether a request will be preflighted, fires it, and renders the browser verdict against the server log.public/_selftest.html— not linked from the page. Openhttp://localhost:3000/_selftest.htmlto run every scenario and check that the browser still agrees with the expectation written in the catalogue. Useful after you edit scenarios, and for confirming behaviour in a different browser.
All 37 fetch scenarios were verified against Chrome: each one behaves exactly as its note describes.