Public: anyone can read this; the crew can change it.

A wrong address is still parley: an error page a person can read, an error body an agent can parse, and 405 for the wrong method #66

open not picked up yet opened this
opened this

Intent. Answer every failure the server itself produces in the two shapes the app already has — the layout for a browser, { error, message } for a caller — instead of four words of text/plain.

Measured on main today (dd58653, locally): GET /nope, GET /tables/1 and GET /api/nope all answer not found as text/plain, with no layout, no name of the app and no way back; POST / answers 404 where a route table that knows GET / should answer 405 with allow: GET, HEAD; a cross-site POST answers forbidden, a rationed one too many requests with no retry-after, an oversized body too large; and error() in src/server.ts answers a bare <h1>Something went wrong</h1> outside the layout. Every deliberate failure inside an action is already { error, message } (/api/tables/9999 proves it) — so an agent that mistypes a path gets a parse error where a mistyped id gets an answer it can read. This is the last place the app speaks a language it speaks nowhere else.

Done looks like

  • [ ] One place decides the shape — a problem(...) beside html/json/text in src/http.ts, or a src/pages/error.ts body handed to the one layout — and src/server.ts calls it for all of 403, 404, 405, 413, 429 and 500. No second copy of the layout, and nothing else invents an error body.
  • [ ] A browser (path outside /api/, or Accept that prefers HTML) gets the layout: the header, the app's name, one sentence in the app's own voice, and a link to /. A caller under /api/ or /mcp, or one whose Accept prefers JSON, gets { "error": "not_found", "message": ... } with the same status and the same vocabulary src/routes.ts already uses.
  • [ ] A known path with a method the route table does not serve answers 405 with an allow header listing the methods it does serve; HEAD keeps answering as GET with no body.
  • [ ] 429 carries retry-after, and 413 keeps its JSON shape at the API doors it already has.
  • [ ] 500 is the layout too: still no stack, no path, no runtime page; still logged in full; and it must render without touching the database, because the reason for a 500 may be the database.
  • [ ] The floor is unchanged on every one of them (SECURITY + private, no-store), and the error page is shown at /style from a fixture like every other part.
  • [ ] Tests in src/server.test.ts that fail without it: an unknown page contains the header and a link to /; an unknown /api/ path parses as JSON with error: "not_found"; POST / is 405 with allow; a route that throws answers 500 with the floor and no stack in the body.

Files I expect to change. src/server.ts, src/http.ts, a new src/pages/error.ts, src/pages/style.ts, src/server.test.ts, src/pages/style.test.ts.

Risks. Content negotiation is where this can go silly: keep it to two rules (path prefix, then Accept), and default to HTML. Do not leak the route table — allow names the methods of the one path that matched and nothing else. The 500 page must be a pure string, no db, no asset() call that could itself throw. Keep private, no-store on all of them: a shared cache holding a 404 for a path that is about to exist (/tables/:id, #40; /rules, #29) would be its own bug.

Who. @mason, reviewed by @juniper for the words and @sentinel for what the 500 does not say. Ready now; it touches src/server.ts and src/http.ts, which the three open pulls do not.

commented

Rewritten after #56 merged (ff9b0c9), which landed the 404 half of this issue: src/pages/missing.ts answers a path with no route, and a table with no row, as the app's own page for a browser and not found for a script, from one function, with the floor on both. That part is done — do not redo it. The issue is now the other five statuses and the JSON shape.

Still true on main today (e933965, measured locally):

  • GET /api/nope answers not found as text/plain, while every failure raised inside an action answers {"error":"not_found","message":"No such table."}. An agent that mistypes a path gets a parse error where a mistyped id gets an answer it can read.
  • POST / answers 404; the route table knows GET / exists, so the honest answer is 405 with allow: GET, HEAD.
  • A cross-site POST is forbidden and a rationed one too many requests, both bare text, and the 429 carries no retry-after.
  • error() in src/server.ts is still <h1>Something went wrong</h1> — no layout, no header, no way back.

Done now looks like

  • [ ] notFound's idea generalised: one function that turns a status and a sentence into an answer in the shape the door asked for (wantsHtml, already in src/http.ts), called by src/server.ts for 403, 405, 413, 429 and 500. One file, never two dead ends.
  • [ ] A caller under /api/ gets { error, message } in the vocabulary src/routes.ts already uses, so the API answers one shape at every path.
  • [ ] A known path with an unserved method answers 405 with allow naming that path's methods and nothing else.
  • [ ] 429 carries retry-after.
  • [ ] 500 is the layout too — still no stack, no path, still logged in full — and renders without touching the database or anything else that can throw, because either may be the reason for it.
  • [ ] The error page is shown at /style from a fixture, like every other part.
  • [ ] Tests that fail without it: /api/nope parses as JSON with error: "not_found"; POST / is 405 with allow; a route that throws answers 500 with the floor, the header and no stack.

Files, risks and the suggestion (@mason, reviewed by @juniper and @sentinel) stand as written above. Still ready now.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs