Public: anyone can read this; the crew can change it.
Every wrong turn answers in the app's own voice: 404, 405, 413, 429 and 500 #13
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error)
API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN)
Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: API Error: Can't reach the API server — check your internet or DNS (EAI_AGAIN) (api_error)
Amendment from the planner, with today's evidence. Fetched from production just now:
$ curl -s -D- -H 'accept: text/html' https://quorum--forge-trevato-apps.open-platform.sh/nope HTTP/2 404 content-type: text/plain; charset=utf-8 content-length: 9 not found
So the body is still nine bytes of plain text for a browser, and the issue stands exactly as written. Two things have settled since it was filed and resolve its open questions: #12 has merged, so layout lives in src/pages/layout.ts — use it and do not revive src/page.ts; and #20 has merged, so the 404 page should sound like the first-visit page rather than inventing a second voice.
Added criterion: the HTML refusal carries cache-control: no-store, so a CDN never caches one person's 403 or 429 for the next person. The floor headers are already on the 404 above — keep them there and let #23's route-table test be the thing that proves it, rather than writing a second list here.
A browser that mistypes a path gets
not foundas bare plain text today, and an unsupported method gets a 404 that says nothing about what is allowed. Every refusal should look like this app and tell a script the same thing in JSON.What done looks like
src/http.tsgains oneproblem(request, status, message, extra?)that answers HTML for a browser (accept:preferstext/html) and{error, message}JSON for anything else, with the security floor on both and no stack, path or runtime detail in either.layout—src/pages/layout.tsif the pull for #2 has merged,src/page.tsif it has not — with the title, one sentence, and a link back to/. No new page module.src/server.tsuses it for: no route (404), a path that exists under another method (405, with anAllowheader naming the methods that do work), a cross-origin write (403), a body over the cap (413) and too many writes (429, withretry-afterin seconds from the bucket). Theerror(...)handler answers 500 the same way and still logs the detail to the server log only.match()insrc/routes.tsgrows a way to ask which methods a path supports, so the 405 is honest rather than guessed; it stays a pure function over the route table.HEADstill answers exactly likeGET, including for these statuses, and carries no body.429names the wait without leaking another person's usage, and the rate limiter's numbers do not move in this issue.src/server.test.ts:GET /nopefrom a browser (accept: text/html) is 404 HTML carrying the layout's title; the same path withaccept: application/jsonis 404 JSON;DELETE /is 405 withAllow: GET; a cross-origin POST is 403 in the caller's shape; an over-cap body is 413; the bucket exhausted gives 429 withretry-after; every one of them still carriescontent-security-policyandx-content-type-options.bun testis green and no route's happy path changes.Files I expect to change:
src/http.ts,src/server.ts,src/routes.ts,src/server.test.ts, and one import from whichever module holdslayout.Risks: an error page is the easiest place to leak — render nothing from the request but the status, never the path the person asked for, and never an exception's message. Keep
problemout of the action layer: aFailurefrom an action still goes throughanswer, and this is only for what never reached one. If pull #12 lands first, rebase ontosrc/pages/layout.tsrather than revivingsrc/page.ts.Fit: Hearth — the panel that tells you plainly what happened. A person assigns; I do not.