Every refusal at the front door is a page of this shop with a way out of it, not a line of plain text.
What the preview does today.src/server.ts answers 403 forbidden (not same-origin), 403 that form was not from here — go back and try again (CSRF), 415 expected a form, 400 bad form and 500 something went wrong as text/plain: no header, no stylesheet, no link, on a white page. The shopper most likely to meet one is the one whose tab sat open until the session cookie rolled over — mid-purchase, with a full cart. shop.tooMany already renders a proper 429 page in the shop's shell; the front door should use the same manners.
Done looks like
[ ] One small module (src/pages/oops.ts) renders a shop-shell page from a status and a sentence, and every refusal in src/server.ts goes through it: same-origin 403, CSRF 403, 415, 400, 500, and the 413 that #28 introduces.
[ ] The CSRF page says what happened in a person's words, links to /cart and to /, and the response still sets the session cookie, so going back and submitting again works rather than failing the same way twice.
[ ] The 500 page never shows an exception, a path or a stack; console.error keeps everything the log needs.
[ ] Each response keeps its status, carries content-type: text/html; charset=utf-8 and cache-control: no-store, and still leaves through finish() with the security floor on it.
[ ] Machine callers are not dressed up: POST /webhooks/stripe (a raw route) still answers plain text, and a request whose accept header does not mention text/html still gets a line.
[ ] Tests in src/server.test.ts for each refusal — status, content type, the shop's header present in the HTML, and no exception text in the 500 — each failing before the change.
[ ] The README's note on what a shopper sees when something goes wrong.
Files I expect to change:src/server.ts, src/pages/oops.ts (new), src/http.ts, src/shop.ts (so 429 and these share one page), src/server.test.ts, README.md.
Risks. Two of these refusals happen before a Ctx exists; render from what is known (the stylesheet path, the session about to be set) rather than reaching for a cart that has not been decoded. Keep the security headers where they are — in finish() — so an error path cannot be the one response that misses them. Content negotiation must not become a guess: check for text/html in accept and default to the page for a browser, the line for everything else.
Waits for: #28 — its 413 is the fifth refusal, and this page should be written once.
Whose brief fits:mason to build it, juniper to read the pages on a phone.
Every refusal at the front door is a page of this shop with a way out of it, not a line of plain text.
What the preview does today.
src/server.tsanswers403 forbidden(not same-origin),403 that form was not from here — go back and try again(CSRF),415 expected a form,400 bad formand500 something went wrongastext/plain: no header, no stylesheet, no link, on a white page. The shopper most likely to meet one is the one whose tab sat open until the session cookie rolled over — mid-purchase, with a full cart.shop.tooManyalready renders a proper 429 page in the shop's shell; the front door should use the same manners.Done looks like
src/pages/oops.ts) renders a shop-shell page from a status and a sentence, and every refusal insrc/server.tsgoes through it: same-origin 403, CSRF 403, 415, 400, 500, and the 413 that #28 introduces./cartand to/, and the response still sets the session cookie, so going back and submitting again works rather than failing the same way twice.console.errorkeeps everything the log needs.content-type: text/html; charset=utf-8andcache-control: no-store, and still leaves throughfinish()with the security floor on it.POST /webhooks/stripe(arawroute) still answers plain text, and a request whoseacceptheader does not mentiontext/htmlstill gets a line.src/server.test.tsfor each refusal — status, content type, the shop's header present in the HTML, and no exception text in the 500 — each failing before the change.Files I expect to change:
src/server.ts,src/pages/oops.ts(new),src/http.ts,src/shop.ts(so 429 and these share one page),src/server.test.ts,README.md.Risks. Two of these refusals happen before a
Ctxexists; render from what is known (the stylesheet path, the session about to be set) rather than reaching for a cart that has not been decoded. Keep the security headers where they are — infinish()— so an error path cannot be the one response that misses them. Content negotiation must not become a guess: check fortext/htmlinacceptand default to the page for a browser, the line for everything else.Waits for: #28 — its 413 is the fifth refusal, and this page should be written once.
Whose brief fits: mason to build it, juniper to read the pages on a phone.