# 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. ```bash node server.js # or: npm start open http://localhost:3000 ``` Then, in this order: 1. Open DevTools → Network and leave it visible. Every "blocked" response is still there, which is the point of the first scenario. 2. Click **Set cookies on API** once, so the credentials scenarios have cookies to send. 3. 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 1. **The basic rule** — CORS only applies cross-origin, and it gates reading the response, not sending the request. Includes the ways `Allow-Origin` is commonly written wrong: a comma-separated list, a trailing slash, unconditional reflection. 2. **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`. The `Max-Age` card is worth running twice: the second run has no OPTIONS. 3. **Credentials and wildcards** — the `*`-with-credentials rule, `Allow-Credentials` on 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. 4. **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. 5. **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. 6. **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. Open `http://localhost:3000/_selftest.html` to 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.