docs(porch-routing): brainstorm story 5 (routing + response ergonomics) to ready
- five decisions: head auto-registers with opt-out (+ patch/options/all); request ids mirror limiter trust model with a NON-crypto source; per-route body_limit is a SECOND check after routing (global BODY_MAX stays the pre-routing ceiling, over-limit = 413); Route fields are corpus-free; Vary accumulates by comma-join - key finding: story 5 has NO upstream dependency, not even iteration 2 -- request ids are not secrets, so a non-crypto source (time.ticks+counter) keeps it startable today; the one porch slice buildable right now - three story assumptions corrected: per-route limit cannot replace the global (body read before routing); the container-owned-move corpus fixture has its OWN Route (adding fields is free); Vary needs no iteration 2 - validated against .dev/reference/fiber; zero language enhancement. Board synced (cherry picked from commit 0589a13db1f3b7c220d9d9fdc76142af6a63c390)
This commit is contained in:
parent
274f7c5361
commit
6b820fdd5f
2 changed files with 98 additions and 55 deletions
|
|
@ -1195,9 +1195,9 @@ the language arc as v1 history.
|
||||||
### ▸ porch — the web framework track
|
### ▸ porch — the web framework track
|
||||||
|
|
||||||
New 2026-08-26, from [the Fiber v3.5.0 parity study](../plan/exploration/fiber/00-fiber-parity.md).
|
New 2026-08-26, from [the Fiber v3.5.0 parity study](../plan/exploration/fiber/00-fiber-parity.md).
|
||||||
Supersedes language iteration 39, now a pointer. **Stories 2, 3 and 4 are
|
Supersedes language iteration 39, now a pointer. **Stories 2, 3, 4 and 5 are
|
||||||
`ready` (brainstormed 2026-09-06, forks locked, validated against
|
`ready` (brainstormed 2026-09-06, forks locked, validated against
|
||||||
`.dev/reference/fiber`); 5–8 remain `refine`.** Ordered by dependency; the
|
`.dev/reference/fiber`); 6–8 remain `refine`.** Ordered by dependency; the
|
||||||
first slice is deliberately the cheapest so the store pattern and gate shape are
|
first slice is deliberately the cheapest so the store pattern and gate shape are
|
||||||
proven before the runtime and `Resp` are touched.
|
proven before the runtime and `Resp` are touched.
|
||||||
|
|
||||||
|
|
@ -1207,7 +1207,7 @@ proven before the runtime and `Resp` are touched.
|
||||||
| 2 | [Randomness and cookies](porch/02-randomness-and-cookies.md) | ✅ **`ready` 2026-09-06** — the foundation; the reference read settled that **exactly one language enhancement is needed**. Phase A is that language work: a bare-name `random_bytes(n) -> Bytes` builtin in the compiler's crypto-family table (`emit.ml` `b_*` + `types.ml` function list — **not** `wob.h`'s module enum; next free id `84`/`90`, confirm before use), `getrandom(2)`-sourced, refuses loudly. Then `Resp` gains `cookies: multi SetCookie` beside the unchanged `headers` map (the map can't emit two `Set-Cookie` lines; `multi` already exists), `Cookie:` parsing (structural 400 in `parse_request`, on-demand `cookie()` helper), and signed cookies (`base64(value).base64(mac)`, app-supplied key) |
|
| 2 | [Randomness and cookies](porch/02-randomness-and-cookies.md) | ✅ **`ready` 2026-09-06** — the foundation; the reference read settled that **exactly one language enhancement is needed**. Phase A is that language work: a bare-name `random_bytes(n) -> Bytes` builtin in the compiler's crypto-family table (`emit.ml` `b_*` + `types.ml` function list — **not** `wob.h`'s module enum; next free id `84`/`90`, confirm before use), `getrandom(2)`-sourced, refuses loudly. Then `Resp` gains `cookies: multi SetCookie` beside the unchanged `headers` map (the map can't emit two `Set-Cookie` lines; `multi` already exists), `Cookie:` parsing (structural 400 in `parse_request`, on-demand `cookie()` helper), and signed cookies (`base64(value).base64(mac)`, app-supplied key) |
|
||||||
| 3 | [Sessions](porch/03-sessions.md) | ✅ **`ready` 2026-09-06** — after 2. Six decisions locked: row is a pure auth primitive (id/principal/created_at/last_seen, no payload bag); **wall-clock `time.now`, not monotonic `time.ticks`** (sessions survive restart); rotation = login always mints a fresh id (no anon-session model); throttled `last_seen` touch at `idle/20` (not a WAL write per request); `Session` writes `req.principal`; config refuses absolute < idle. Pure `.wo` on iteration 2 + the `@table` engine — no new runtime work |
|
| 3 | [Sessions](porch/03-sessions.md) | ✅ **`ready` 2026-09-06** — after 2. Six decisions locked: row is a pure auth primitive (id/principal/created_at/last_seen, no payload bag); **wall-clock `time.now`, not monotonic `time.ticks`** (sessions survive restart); rotation = login always mints a fresh id (no anon-session model); throttled `last_seen` touch at `idle/20` (not a WAL write per request); `Session` writes `req.principal`; config refuses absolute < idle. Pure `.wo` on iteration 2 + the `@table` engine — no new runtime work |
|
||||||
| 4 | [CSRF](porch/04-csrf.md) | ✅ **`ready` 2026-09-06** — after 2 + 3. Five decisions locked: fiber's **hybrid** transport (session-stored `CsrfToken` @table keyed by token + double-submit cookie, both must pass; no CSRF for sessionless apps); opt-in single-use (checkout the example, admin multi-use); a double-click yields a distinct `SPENT` refusal with **no coupling to the lang-41-blocked idempotency**; trusted origin/referer/`Sec-Fetch-Site` as the second layer; refusal classes distinguishable in logs, opaque in body. Plain `@table` CRUD — no actor pool, not blocked on lang-41 |
|
| 4 | [CSRF](porch/04-csrf.md) | ✅ **`ready` 2026-09-06** — after 2 + 3. Five decisions locked: fiber's **hybrid** transport (session-stored `CsrfToken` @table keyed by token + double-submit cookie, both must pass; no CSRF for sessionless apps); opt-in single-use (checkout the example, admin multi-use); a double-click yields a distinct `SPENT` refusal with **no coupling to the lang-41-blocked idempotency**; trusted origin/referer/`Sec-Fetch-Site` as the second layer; refusal classes distinguishable in logs, opaque in body. Plain `@table` CRUD — no actor pool, not blocked on lang-41 |
|
||||||
| 5 | [Routing + response ergonomics](porch/05-routing-response-ergonomics.md) | ⬜ **independent, any time** — `patch`/`options`/`head`/`all`, named routes + URL building, per-route body limit (today `BODY_MAX` is one compile-time number), request ids, `Location`/`Vary`/`Attachment`, and q-value ranking (retires a standing 🔶) |
|
| 5 | [Routing + response ergonomics](porch/05-routing-response-ergonomics.md) | ✅ **`ready` 2026-09-06** — **independent, any time; NO upstream dependency (not even iteration 2)**. Five decisions: `head` auto-registers with an opt-out (+ `patch`/`options`/`all`); request ids mirror the limiter's trust model with a **non-crypto** source (so no CSPRNG dependency); per-route `body_limit` is a **second check after routing** (global `BODY_MAX` stays the pre-routing ceiling, over-limit = 413); adding `name`/`body_limit` to `Route` is corpus-free; `Vary` accumulates by comma-join. Plus named routes + runtime-checked URL building, q-value ranking (retires the 🔶) |
|
||||||
| 6 | [Streaming core](porch/06-streaming-core.md) | ⬜ the riskiest and highest-leverage slice: incremental writes + chunked framing + an explicit commit point. `serialize()` always emits `Content-Length` today. Chunked REQUEST bodies are deliberately refused (request smuggling) and that refusal must survive |
|
| 6 | [Streaming core](porch/06-streaming-core.md) | ⬜ the riskiest and highest-leverage slice: incremental writes + chunked framing + an explicit commit point. `serialize()` always emits `Content-Length` today. Chunked REQUEST bodies are deliberately refused (request smuggling) and that refusal must survive |
|
||||||
| 7 | [SSE + compression](porch/07-sse-and-compression.md) | ⬜ after 6. SSE fits the actor/fiber model unusually well; compression carries a real fork — pure-`.wo` DEFLATE (now expressible after iteration 36's bit operators) vs a C builtin. CRC32 finally gets its consumer |
|
| 7 | [SSE + compression](porch/07-sse-and-compression.md) | ⬜ after 6. SSE fits the actor/fiber model unusually well; compression carries a real fork — pure-`.wo` DEFLATE (now expressible after iteration 36's bit operators) vs a C builtin. CRC32 finally gets its consumer |
|
||||||
| 8 | [Static files + lifecycle](porch/08-static-and-lifecycle.md) | ⬜ static half after 6. Byte ranges, `Last-Modified`/`Cache-Control`, index resolution, listing off-by-default, shutdown hooks (the ledger's "no user teardown hooks yet"), plus healthcheck/favicon/redirect/rewrite/skip |
|
| 8 | [Static files + lifecycle](porch/08-static-and-lifecycle.md) | ⬜ static half after 6. Byte ranges, `Last-Modified`/`Cache-Control`, index resolution, listing off-by-default, shutdown hooks (the ledger's "no user teardown hooks yet"), plus healthcheck/favicon/redirect/rewrite/skip |
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,28 @@
|
||||||
track: porch
|
track: porch
|
||||||
iteration: "5"
|
iteration: "5"
|
||||||
status: pending
|
status: pending
|
||||||
readiness: refine
|
readiness: ready
|
||||||
---
|
---
|
||||||
|
|
||||||
# porch 5 — routing and response ergonomics: the parity that is merely missing
|
# porch 5 — routing and response ergonomics: the parity that is merely missing
|
||||||
|
|
||||||
> Part of [Story — `porch`, the writeonce web framework](00-story.md).
|
> Part of [Story — `porch`, the writeonce web framework](00-story.md).
|
||||||
> Source: [the Fiber parity study](../../plan/exploration/fiber/00-fiber-parity.md) §5.
|
> Source: [the Fiber parity study](../../plan/exploration/fiber/00-fiber-parity.md) §5,
|
||||||
|
> re-checked 2026-09-06 against `.dev/reference/fiber` (v3, `3ca9a9d`):
|
||||||
|
> `DisableHeadAutoRegister`, `Name()`/`GetRouteURL()`, `BodyLimit`, and the
|
||||||
|
> `requestid` middleware's trust model.
|
||||||
>
|
>
|
||||||
> Independent of iterations 2–4 and of the streaming seam — startable at any
|
> Independent of iterations 2–4 and of the streaming seam — and, as the
|
||||||
> time, and a reasonable slice to interleave when the risky work needs a break.
|
> brainstorm confirmed, with **no upstream dependency at all** (not even
|
||||||
> Nothing here is hard; all of it is felt.
|
> iteration 2): startable at any time, a reasonable slice to interleave when the
|
||||||
|
> risky work needs a break. Nothing here is hard; all of it is felt.
|
||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
- **The rest of the method helpers.** `App` has `get`/`post`/`put`/`delete_`.
|
- **The rest of the method helpers.** `App` has `get`/`post`/`put`/`delete_`.
|
||||||
A `Route { method: "PATCH" }` literal already works, so this is registration
|
A `Route { method: "PATCH" }` literal already works, so this is registration
|
||||||
ergonomics rather than capability — but writing the literal by hand for
|
ergonomics rather than capability. Add `patch`, `options`, `head`, and an
|
||||||
`PATCH` while `get` exists is the kind of asymmetry that makes a framework
|
`all`.
|
||||||
feel unfinished. Add `patch`, `options`, `head`, and an `all`.
|
|
||||||
- **Named routes and reverse routing.** Fiber has `Name()` and `GetRouteURL()`.
|
- **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
|
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
|
compiler checks — and the site is exactly the app where a renamed path breaks
|
||||||
|
|
@ -30,60 +33,101 @@ readiness: refine
|
||||||
and the JSON route wants a much smaller one than the upload route can live
|
and the JSON route wants a much smaller one than the upload route can live
|
||||||
with.
|
with.
|
||||||
- **Request ids.** `req.ctx` already exists to carry one; there is no generator
|
- **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 no middleware. It is the difference between logs you can correlate and
|
||||||
and it is the difference between logs you can correlate and logs you cannot.
|
logs you cannot.
|
||||||
- **The response helpers written by hand today.** `Location`, `Vary`,
|
- **The response helpers written by hand today.** `Location`, `Vary`,
|
||||||
`Attachment`/`Download`, and a content-negotiated `format` dispatch on top of
|
`Attachment`/`Download`, and a content-negotiated `format` dispatch on top of
|
||||||
the existing `accepts()`. Also q-value *ranking*, which the ledger has carried
|
the existing `accepts()`. Also q-value *ranking*, which the ledger has carried
|
||||||
as a known 🔶 since the negotiation slice landed.
|
as a known 🔶 since the negotiation slice landed.
|
||||||
|
|
||||||
|
## Decisions locked (brainstorm 2026-09-06)
|
||||||
|
|
||||||
|
1. **`head` auto-registers alongside `get`, with an opt-out.** Matches fiber
|
||||||
|
(`DisableHeadAutoRegister` proves the default is auto and people want the
|
||||||
|
knob). `serialize()` already produces headers-only with a GET's
|
||||||
|
`Content-Length`, so this is pure registration — the behaviour exists, this
|
||||||
|
makes it reachable without a hand-written literal. `patch`/`options`/`all`
|
||||||
|
are plain additions; `all` registers one handler for every method.
|
||||||
|
2. **Request ids mirror the limiter's trust conclusion, and use a non-crypto
|
||||||
|
source.** Default (`trust_inbound` off): always generate a fresh id and
|
||||||
|
ignore any inbound `X-Request-Id`, because an open port can forge correlation
|
||||||
|
ids and poison logs — exactly what the limiter's `trust_proxy` guards.
|
||||||
|
Opt-in (behind the mandated proxy): honor an inbound `X-Request-Id` for
|
||||||
|
cross-hop tracing, generate when absent. The id is minted from a non-crypto
|
||||||
|
unique source (`time.ticks` plus a per-process counter), **not** iteration
|
||||||
|
2's `random_bytes` — a request id is not a secret, and this keeps the whole
|
||||||
|
iteration free of upstream dependencies. It is written to `req.ctx` and
|
||||||
|
echoed in the response header, and the logging middleware includes it.
|
||||||
|
3. **The per-route body limit is a second check after routing; the global
|
||||||
|
`BODY_MAX` stays as the pre-routing ceiling.** The body is read in
|
||||||
|
`parse_request` before the route is known (`parse.wo`), so something must cap
|
||||||
|
bytes first — the global remains that DoS ceiling. A `body_limit: Int` field
|
||||||
|
on `Route` (default the global, and never above it) is checked in dispatch
|
||||||
|
against `len(req.body)`; over-limit is a 413. This corrects the story's
|
||||||
|
implication that the per-route limit replaces the global — it cannot, because
|
||||||
|
routing happens after the read.
|
||||||
|
4. **Adding fields to `Route` is free of the conformance corpus.** Fork 3 warned
|
||||||
|
the corpus pins `Route`'s ownership shape; it does not — `container-owned-move`
|
||||||
|
defines its own local `Route`/`App` to pin move-on-push and is decoupled from
|
||||||
|
porch's. The new fields (`name: Text`, `body_limit: Int`) are scalar/Text,
|
||||||
|
copy-stored, with no ownership complication for the owned `h: Handler`.
|
||||||
|
5. **`Vary` accumulates by comma-joining in the existing header map — no
|
||||||
|
iteration 2 needed.** One `Vary: A, B` header keeps the iteration truly
|
||||||
|
independent; the story's tie to iteration 2's repeated-header work is not
|
||||||
|
required for this.
|
||||||
|
|
||||||
## Phases
|
## Phases
|
||||||
|
|
||||||
### Phase A — method helpers and route introspection
|
### Phase A — method helpers and route introspection
|
||||||
|
|
||||||
- The missing registration helpers, including `all`, and decide whether `head`
|
- Add `patch`, `options`, `head`, `all` to `App`, each pushing a `Route` literal
|
||||||
auto-registers alongside `get` (Fiber has `DisableHeadAutoRegister`, which
|
as `get`/`post` already do; `all` registers the handler for every method.
|
||||||
tells you the default is auto and that people want it off).
|
`head` auto-registration (decision 1) is wired here with its opt-out.
|
||||||
- Route introspection — list the table — because it is nearly free once routes
|
- Route introspection — list the table — nearly free once routes are a
|
||||||
are already a `multi Route`, and it is what makes a startup banner or a
|
`multi Route`, and what makes a startup banner or a route-dump flag possible.
|
||||||
route-dump flag possible.
|
|
||||||
- Verify: each method dispatches; `405` still carries a correct `Allow` built
|
- Verify: each method dispatches; `405` still carries a correct `Allow` built
|
||||||
from the real table; existing routes unchanged.
|
from the real table (the `app.wo` dispatch loop already assembles it);
|
||||||
|
existing routes unchanged.
|
||||||
|
|
||||||
### Phase B — named routes and URL building
|
### Phase B — named routes and URL building
|
||||||
|
|
||||||
- A name on `Route`, a lookup, and a builder that fills `:param` captures.
|
- A `name` field on `Route`, a lookup by name, and a builder that fills `:param`
|
||||||
- Decide the failure mode for a missing or extra parameter. A silently wrong URL
|
captures against the route's pattern.
|
||||||
is worse than a trap, and this is a compile-time-checkable shape only once
|
- The failure mode for a missing or extra parameter is a loud runtime refusal,
|
||||||
language iteration 29's `@derive` exists — so for now it is a runtime check
|
not a plausible-looking wrong URL. This is compile-time-checkable only once
|
||||||
and should say so.
|
language iteration 29's `@derive` exists — so it is a runtime check now and
|
||||||
- Migrate the site's internal links onto it, which is the proof it is usable.
|
says so.
|
||||||
|
- Migrate the site's internal links onto the builder, which is the proof it is
|
||||||
|
usable.
|
||||||
- Verify: every site link resolves through the builder; a wrong parameter set is
|
- Verify: every site link resolves through the builder; a wrong parameter set is
|
||||||
refused loudly.
|
refused loudly.
|
||||||
|
|
||||||
### Phase C — per-route body limits and request ids
|
### Phase C — per-route body limits and request ids
|
||||||
|
|
||||||
- Move the limit from a module constant to route-level configuration with the
|
- Add `body_limit: Int` to `Route`, defaulting to the global `BODY_MAX`, checked
|
||||||
current value as the default, so no existing app changes behaviour.
|
in dispatch after the route matches (decision 3); an oversized body is a 413
|
||||||
- A request-id middleware writing into `req.ctx`, and settle whether an inbound
|
at that route while the global ceiling still bounds the pre-routing read.
|
||||||
header is trusted (fork 2).
|
- A request-id middleware (decision 2): non-crypto id, trust model mirroring the
|
||||||
|
limiter, written to `req.ctx` and the response header.
|
||||||
- Thread the id into the logging middleware's output, since a request id nothing
|
- Thread the id into the logging middleware's output, since a request id nothing
|
||||||
logs is decoration.
|
logs is decoration.
|
||||||
- Verify: an oversized body is refused per-route; the id appears in logs and is
|
- Verify: an oversized body is refused per-route; a request under the global but
|
||||||
stable across a request's lifetime.
|
over a route's tighter limit is refused only at that route; the id appears in
|
||||||
|
logs and the response header and is stable across a request's lifetime.
|
||||||
|
|
||||||
### Phase D — response helpers and negotiation ranking
|
### Phase D — response helpers and negotiation ranking
|
||||||
|
|
||||||
- `Location`, `Vary`, `Attachment`/`Download`, and a `format`-style dispatch
|
- `Location`, `Vary` (comma-join accumulation, decision 5), `Attachment`/
|
||||||
choosing a builder from `accepts()`.
|
`Download`, and a `format`-style dispatch choosing a builder from `accepts()`.
|
||||||
- Rank q-values properly instead of stripping them, retiring the ledger's 🔶.
|
- Rank q-values properly instead of stripping them, retiring the ledger's 🔶:
|
||||||
- Verify: `Vary` accumulates rather than overwrites (which the iteration-2
|
parse `Accept` into type/q pairs, and pick the highest-q acceptable match.
|
||||||
repeated-header work makes possible); negotiation picks the highest-q match,
|
- Verify: `Vary` accumulates rather than overwrites; negotiation picks the
|
||||||
not the first.
|
highest-q match, not the first listed.
|
||||||
|
|
||||||
### Phase E — the gate and the ledger
|
### Phase E — the gate and the ledger
|
||||||
|
|
||||||
- Both serving gates, the ledger rows, the board entry.
|
- Both serving gates, the ledger rows, the board entry, standup questions
|
||||||
|
answered including the `.dev/reference` projects used.
|
||||||
- Verify: `just web-app`, `just site`, `just linkcheck` green.
|
- Verify: `just web-app`, `just site`, `just linkcheck` green.
|
||||||
|
|
||||||
## Acceptance Criteria
|
## Acceptance Criteria
|
||||||
|
|
@ -93,23 +137,26 @@ readiness: refine
|
||||||
yields `405` with an `Allow` listing exactly the registered methods.
|
yields `405` with an `Allow` listing exactly the registered methods.
|
||||||
- **Given** `head` auto-registration, **when** a `HEAD` request hits a `GET`
|
- **Given** `head` auto-registration, **when** a `HEAD` request hits a `GET`
|
||||||
route, **then** the response is headers-only with the `Content-Length` a `GET`
|
route, **then** the response is headers-only with the `Content-Length` a `GET`
|
||||||
would have sent — the behaviour `serialize()` already implements, now
|
would have sent; **and** with auto-registration disabled the `GET` route
|
||||||
reachable by registration.
|
answers `GET` only.
|
||||||
- **Given** a named route with `:param` captures, **when** a URL is built with
|
- **Given** a named route with `:param` captures, **when** a URL is built with
|
||||||
the right parameters, **then** it matches that route's pattern exactly;
|
the right parameters, **then** it matches that route's pattern exactly;
|
||||||
**and** a wrong or missing parameter is refused rather than producing a
|
**and** a wrong or missing parameter is refused rather than producing a
|
||||||
plausible-looking wrong URL.
|
plausible-looking wrong URL.
|
||||||
- **Given** two routes with different body limits, **when** a body exceeding the
|
- **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
|
smaller arrives at each, **then** it is refused (413) at the small route and
|
||||||
accepted at the large one.
|
accepted at the large one.
|
||||||
- **Given** no per-route limit, **when** a request arrives, **then** the
|
- **Given** no per-route limit, **when** a request arrives, **then** the
|
||||||
previous global limit applies unchanged.
|
previous global limit applies unchanged.
|
||||||
|
- **Given** a request-id middleware with inbound trust off, **when** a request
|
||||||
|
arrives carrying `X-Request-Id`, **then** a fresh id is generated and the
|
||||||
|
inbound one ignored; **and** with trust on, the inbound id is honored.
|
||||||
- **Given** a request-id middleware, **when** a request is handled, **then** the
|
- **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.
|
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
|
- **Given** an `Accept` header with q-values out of order, **when** negotiation
|
||||||
runs, **then** the highest-q acceptable type wins — not the first listed.
|
runs, **then** the highest-q acceptable type wins — not the first listed.
|
||||||
- **Given** two `Vary` contributions from different middleware, **when** the
|
- **Given** two `Vary` contributions from different middleware, **when** the
|
||||||
response leaves, **then** both appear.
|
response leaves, **then** both appear in one comma-joined `Vary`.
|
||||||
|
|
||||||
## Out Of Scope
|
## Out Of Scope
|
||||||
|
|
||||||
|
|
@ -123,21 +170,17 @@ readiness: refine
|
||||||
- **Compile-time-checked URL building.** The typed version needs language
|
- **Compile-time-checked URL building.** The typed version needs language
|
||||||
iteration [29](../language-runtime-database/29-compile-time-metaprogramming.md).
|
iteration [29](../language-runtime-database/29-compile-time-metaprogramming.md).
|
||||||
Runtime-checked now, upgraded later.
|
Runtime-checked now, upgraded later.
|
||||||
|
- **Cryptographically random request ids.** Not needed — a request id is not a
|
||||||
|
secret (decision 2). If iteration 2 is landed, `random_bytes` is an acceptable
|
||||||
|
alternative source, but this iteration must not depend on it.
|
||||||
- **Streaming responses, `SendFile`, byte ranges** — iterations
|
- **Streaming responses, `SendFile`, byte ranges** — iterations
|
||||||
[6](06-streaming-core.md) and [8](08-static-and-lifecycle.md).
|
[6](06-streaming-core.md) and [8](08-static-and-lifecycle.md).
|
||||||
- **Typed binding of params into a class** — language iteration 29 again.
|
- **Typed binding of params into a class** — language iteration 29 again.
|
||||||
|
|
||||||
## Info
|
## Info
|
||||||
|
|
||||||
Forks the spec must settle:
|
Forks are settled above. This iteration is entirely pure `.wo` and, unusually
|
||||||
|
for the track, has **no upstream dependency** — not the streaming seam, not
|
||||||
1. **Does `head` auto-register?** Fiber's default is yes with an opt-out. Auto is
|
sessions, not even iteration 2's builtin (decision 2 keeps request ids off the
|
||||||
friendlier; explicit is more predictable and never surprises someone
|
CSPRNG). It is the safest slice to pick up at any time, which is exactly why the
|
||||||
debugging why a route they did not register is answering.
|
track lists it as independent.
|
||||||
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.
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue