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

A wrong turn still looks like relay: an HTML 404, a 405 that says so, and a failure that never answers 200 #53

open not picked up yet opened this
opened this

Two ways relay answers today are not answers a person or a script should get: a path that does not exist returns nine bytes of text/plain with no way back to the timeline, and a failure the route table forgot returns HTTP 200 with an error body.

STATUS in src/routes.ts maps four of the five members of Failure; unavailable — which registerApp and rotateAppKey return when no identity key is configured — is missing, so answer() passes undefined as the status and json() falls back to its default of 200. The declaration says Record<Failure, number>, so a type checker would have caught it; nothing type-checks yet (#25), which is what makes this a live bug rather than a typo.

Done looks like

  • [ ] STATUS covers every member of Failure, unavailable answers 503 with a retry-after, and a test walks the union (a list of every failure, asserted to have a status that is not 200) so the next failure added cannot slip through the same gap.
  • [ ] A missing path answers in the shape the caller asked for: HTML through layout() under / and anything that is not an API path, JSON { error: "not_found", message } under /api/ and /mcp, plain text otherwise. The choice is made from the path prefix and the accept header, in one small function, never guessed per route.
  • [ ] The HTML 404 is a page, not a sentence: the same header, the environment pill, a link back to the timeline, the floor from src/http.ts, and status 404. It renders for an anonymous caller without needing anything from the request but its URL.
  • [ ] A path that exists at another method answers 405 with an allow header naming the methods that do exist, instead of today's 404 — match() already knows, since it skips routes by method.
  • [ ] The 500 in src/server.ts renders through the same layout with ANON, still says nothing about the error, still logs the detail, and is proved by a test route that throws — asserting the body carries no stack, no path and no message from the thrown error.
  • [ ] HEAD on a missing path is the same status with no body, and the security floor is on 404, 405 and 500 alike.
  • [ ] src/server.test.ts covers each of the above; nothing about an existing route's answer changes.

Files: src/routes.ts, src/http.ts, src/server.ts, src/page.ts, src/server.test.ts

Risks: the error page must render from nothing — no who, no database, no params — because the handler that reaches it may be the one whose request fell apart; take layout(body, ANON) and keep it free of anything the request supplied. Do not echo the requested path into the 404 page: it is attacker-controlled text, and the page gains nothing from it. Keep json()'s default status as it is; the fix belongs in the table, not in the helper.

Suggested: Scout — small, finished, and it turns the two answers nobody looks at into answers that carry the app's floor and its shape.

commented

One convention to reuse rather than invent: 1be64f6 (#40) added tooManyRequests in src/server.ts, which already chooses a body by door — JSON under /api/ and /mcp, the sentence everywhere else — and json() now takes headers. The 404, the 405 and the 500 should pick their shape the same way, ideally by lifting that choice into one small helper both call, so the app has one rule about what a refusal looks like instead of two that agree by accident.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs