writeonce/docs/stories/porch/05-routing-response-ergonomics.md
shoney.arickathil 01df75245f docs(porch): give the framework its own story track, iterations 1-8
- docs/stories/porch/ — a TRACK folder, not a status folder: status still
  lives only in frontmatter. Adds `track: porch` so a query over
  docs/stories/ can tell a porch 3 from a language 3
- 00-story.md carries the sequence, the dependency graph, and a table of
  what the track explicitly does NOT own (binding -> 29, cache -> 18,
  proxy -> 38, metrics -> 30, TLS/templates -> doctrine)
- eight iterations, each with phases, per-phase tasks, Given/When/Then
  criteria, out-of-scope and the forks a spec must settle:
  1 store-backed middleware (limiter + idempotency — needs nothing new,
    first on purpose so the store pattern is proven cheaply)
  2 randomness + cookies (phase A is language-track: a CSPRNG builtin;
    `Resp.headers` being a map cannot emit two Set-Cookie lines)
  3 sessions   4 CSRF   5 routing/response ergonomics (independent)
  6 streaming core (the seam 7 and 8 wait on; chunked-request refusal
    must survive)   7 SSE + compression   8 static + lifecycle hooks
- language iteration 39 -> status: hold, retitled superseded, with a row
  mapping each of its goals to the porch iteration that took it. Kept, not
  deleted: the Fiber study cites it and its randomness argument is what
  this track is built on
- board gains a porch section; board-views gains porch and both-track
  Dataview queries; porch README and the Fiber study §7 point at the track
- no code blocks in any story (plans carry concept and actions in words);
  linkcheck 0 broken / 0 anchors

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 20:17:33 +02:00

142 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
track: porch
iteration: "5"
status: refine
---
# porch 5 — routing and response ergonomics: the parity that is merely missing
> Part of [Story — `porch`, the writeonce web framework](00-story.md).
> Source: [the Fiber parity study](../../plan/exploration/fiber/00-fiber-parity.md) §5.
>
> Independent of iterations 2–4 and of the streaming seam — startable at any
> time, and a reasonable slice to interleave when the risky work needs a break.
> Nothing here is hard; all of it is felt.
## Goals
- **The rest of the method helpers.** `App` has `get`/`post`/`put`/`delete_`.
A `Route { method: "PATCH" }` literal already works, so this is registration
ergonomics rather than capability — but writing the literal by hand for
`PATCH` while `get` exists is the kind of asymmetry that makes a framework
feel unfinished. Add `patch`, `options`, `head`, and an `all`.
- **Named routes and reverse routing.** Fiber has `Name()` and `GetRouteURL()`.
porch has neither, so every link in the site is a hand-written string that no
compiler checks — and the site is exactly the app where a renamed path breaks
a page silently.
- **A per-route body limit.** `BODY_MAX = 1048576` is one compile-time constant
for the whole server. An upload route and a JSON route want different numbers,
and the JSON route wants a much smaller one than the upload route can live
with.
- **Request ids.** `req.ctx` already exists to carry one; there is no generator
and no middleware. With iteration 2's builtin available this is a few lines,
and it is the difference between logs you can correlate and logs you cannot.
- **The response helpers written by hand today.** `Location`, `Vary`,
`Attachment`/`Download`, and a content-negotiated `format` dispatch on top of
the existing `accepts()`. Also q-value *ranking*, which the ledger has carried
as a known 🔶 since the negotiation slice landed.
## Phases
### Phase A — method helpers and route introspection
- The missing registration helpers, including `all`, and decide whether `head`
auto-registers alongside `get` (Fiber has `DisableHeadAutoRegister`, which
tells you the default is auto and that people want it off).
- Route introspection — list the table — because it is nearly free once routes
are already a `multi Route`, and it is what makes a startup banner or a
route-dump flag possible.
- Verify: each method dispatches; `405` still carries a correct `Allow` built
from the real table; existing routes unchanged.
### Phase B — named routes and URL building
- A name on `Route`, a lookup, and a builder that fills `:param` captures.
- Decide the failure mode for a missing or extra parameter. A silently wrong URL
is worse than a trap, and this is a compile-time-checkable shape only once
language iteration 29's `@derive` exists — so for now it is a runtime check
and should say so.
- Migrate the site's internal links onto it, which is the proof it is usable.
- Verify: every site link resolves through the builder; a wrong parameter set is
refused loudly.
### Phase C — per-route body limits and request ids
- Move the limit from a module constant to route-level configuration with the
current value as the default, so no existing app changes behaviour.
- A request-id middleware writing into `req.ctx`, and settle whether an inbound
header is trusted (fork 2).
- Thread the id into the logging middleware's output, since a request id nothing
logs is decoration.
- Verify: an oversized body is refused per-route; the id appears in logs and is
stable across a request's lifetime.
### Phase D — response helpers and negotiation ranking
- `Location`, `Vary`, `Attachment`/`Download`, and a `format`-style dispatch
choosing a builder from `accepts()`.
- Rank q-values properly instead of stripping them, retiring the ledger's 🔶.
- Verify: `Vary` accumulates rather than overwrites (which the iteration-2
repeated-header work makes possible); negotiation picks the highest-q match,
not the first.
### Phase E — the gate and the ledger
- Both serving gates, the ledger rows, the board entry.
- Verify: `just web-app`, `just site`, `just linkcheck` green.
## Acceptance Criteria
- **Given** a route registered with each new helper, **when** the matching
method arrives, **then** it dispatches; **and** an unmatched method still
yields `405` with an `Allow` listing exactly the registered methods.
- **Given** `head` auto-registration, **when** a `HEAD` request hits a `GET`
route, **then** the response is headers-only with the `Content-Length` a `GET`
would have sent — the behaviour `serialize()` already implements, now
reachable by registration.
- **Given** a named route with `:param` captures, **when** a URL is built with
the right parameters, **then** it matches that route's pattern exactly;
**and** a wrong or missing parameter is refused rather than producing a
plausible-looking wrong URL.
- **Given** two routes with different body limits, **when** a body exceeding the
smaller arrives at each, **then** it is refused at the small route and
accepted at the large one.
- **Given** no per-route limit, **when** a request arrives, **then** the
previous global limit applies unchanged.
- **Given** a request-id middleware, **when** a request is handled, **then** the
same id appears in every log line for that request and in the response header.
- **Given** an `Accept` header with q-values out of order, **when** negotiation
runs, **then** the highest-q acceptable type wins — not the first listed.
- **Given** two `Vary` contributions from different middleware, **when** the
response leaves, **then** both appear.
## Out Of Scope
- **A radix-tree router.** Path matching is a linear scan and the ledger marks
it 🔶 pending a *measurement*. Language iteration 22 built the benchmark
harness but pointed it at the database. Until someone benches the router, this
is an optimisation without evidence.
- **Case-insensitive or strict-slash routing.** Fiber exposes both as config.
porch is case-sensitive and lenient; changing that is a behaviour change for
existing apps and wants its own decision.
- **Compile-time-checked URL building.** The typed version needs language
iteration [29](../language-runtime-database/29-compile-time-metaprogramming.md).
Runtime-checked now, upgraded later.
- **Streaming responses, `SendFile`, byte ranges** — iterations
[6](06-streaming-core.md) and [8](08-static-and-lifecycle.md).
- **Typed binding of params into a class** — language iteration 29 again.
## Info
Forks the spec must settle:
1. **Does `head` auto-register?** Fiber's default is yes with an opt-out. Auto is
friendlier; explicit is more predictable and never surprises someone
debugging why a route they did not register is answering.
2. **Is an inbound request-id header trusted?** Behind the mandated proxy,
trusting it is what makes tracing work across hops. On an open port it lets a
client forge correlation ids and poison logs. `client_ip` and `net.peer`
already exist for exactly this trust decision — reuse that conclusion.
3. **Where does a route's body limit live?** A field on `Route` is the obvious
home but widens a record that the conformance corpus pins the ownership shape
of. Check that fixture before choosing.