web: Middleware, Next, WebApp, the web { } builder and app.answer (step 2 of #94) #776

Open
opened 2026-11-13 23:43:54 +00:00 by orion · 6 comments
Collaborator

Context

Step 2 of the epic #94 (WEB slice 2, std/web core). Step 1 (#575, PR #578) is landed on main: std/web has WebError, Handler ((request: Request) => Task<Result<Response, WebError>>) and the request and response helpers. This step adds the application: the middleware chain, the web { } builder, the adapter to std/http's handler and app.answer(request). No WebTest yet (step 3), no example (step 4).

Source

  • WEB.md sections 3.1, 3.3, 3.8, 3.9: the application, the middleware trait and Next, the error pages, answer.
  • WEB.md section 8, "One question for the checker": a bare function value of invisible captures may not cross into a task, a record carrying one may (probe 8).
  • std/http Handler, Server (std/http/src/server.trb), docs/language/configuration/builders.md for the builder shape.
  • Decision record: comment on #94 (the loop's decisions, incl. assets not in the vocabulary yet and the fallback for Next).

Design

  • trait Middleware { fn around(request: Request, next: Next): Task<Result<Response, WebError>> } and type Next { layers: List<Middleware>, endpoint: Handler, fn run(request) } as in WEB.md section 3.3: run calls the first layer with a Next over the rest, or the endpoint when none is left. A middleware that answers without calling next refuses the request; one that changes the request builds a new Request from the parts of the old one and takes its body over (Request, Response and Body are shared types that own a one-shot stream, see the "Built" note in WEB.md 3.4).
  • WebApp is the builder receiver with exactly three members in this step: wrap(layer: Middleware) (layers run in the order written, the first outermost), handle(handler: Handler), errors(render: (WebError) => Response); no assets (#100). web { } follows the builder rules of builders.md; a block without handle is a compile-time or startup failure with a clear message, a second handle is refused the same way. Name choice stays inside the receiver-before-module rule (probe 7); the receiver has no other members.
  • errors is optional; the default renders WebError.response() of step 1. The hook sees the status and public message only, never the cause (it is logged by the adapter, never shown, WEB.md 3.8).
  • app.answer(request): Task<Result<Response, WebError>> runs layers, handler and, on a Fail, the errors renderer, so for a caller it answers Ok(page) for every WebError; it fails only when the renderer itself fails. (The WEB.md 3.9 sample matches both arms; the docs say which one answer takes.)
  • The adapter: app.serving(): http.Handler (name may follow the repo's participle rule) wraps answer into std/http's Handler (Result<Response, HttpError>), so Server.bind(...) takes it unchanged; a WebError never reaches the transport as an HttpError because answer already rendered it.
  • The probe. First write tests/conformance/web-next-record-capture.trb (record Next carrying endpoint: Handler with invisible captures, awaited inside a task) and run it in the forge CI. If it checks: keep Next as in WEB.md and record "intended, held by this test" in WEB.md section 8. If the checker refuses it: endpoint becomes a trait value (trait Endpoint { fn serve(request): Task<...> } plus an adapter from a Handler closure written at the call site), WEB.md section 3.3 gets a "Built" note, and the probe stays as a negative conformance test with the expected error. Either way the loop records the result in the PR.
  • Touches the seed? No: new members of an existing package, no natives, no std name the compiler looks up by string. If the probe shows a checker gap, the fix is a separate issue, not part of this step.

Acceptance criteria

  • std/web/src/middleware.trb (Middleware, Next) and std/web/src/app.trb (WebApp, web, answer, the std/http adapter) exported from lib.trb; every public item has a doc comment with an example.
  • tests/conformance/web-next-record-capture.trb exists and its outcome (checks, or refused with the expected error) is recorded in WEB.md section 8 and in the PR body.
  • Tests in std/web/tests/app.test.trb (handlers are fns answering a Task awaited at the test file's top level, F13): two layers run outermost first and unwind in reverse; a layer that does not call next stops the chain and the handler is not run; a layer that rebuilds the request is seen below it and not above; a Fail WebError.notFound() from the handler answers Ok with status 404 and the default page; a custom errors renderer is used for 404 and 400; the cause is not in the rendered page; web { } without handle and with two handles is refused.
  • A test serves app.serving() through std/http's server on a loopback port (or the repo's existing in-memory transport used by std/http tests) and gets the 404 page and a 200 page back.
  • docs/standard-library/web.md gains the middleware, web { } and answer sections; skills/ regenerated (torb docs skill docs skills); WEB.md status line and slice table say slice 2 is built up to answer (WebTest and the example remain).
  • Lands as at most two Conventional Commits: feat(std/web): add Middleware and Next (with the probe), feat(std/web): add the web builder and the std/http adapter.

Seed

No. New package members, no natives, no breaking change to a published contract.

Size

One PR, two commits, std/web only plus the probe, WEB.md and the docs page; about 1 h of implementation on top of #575. Sonnet/medium per the loop contract for std/; if the probe forces the trait-value fallback it is still the same PR.

Dependencies

  • Needs: #575 (closed, landed on main), std/http Handler/Server (on main).
  • Unblocks: step 3 of #94 (WebTest), #97 (forms and CSRF), #98 (sessions and cookies), #100 (assets), #103 (std/live).

Part of #94.

## Context Step 2 of the epic #94 (WEB slice 2, `std/web` core). Step 1 (#575, PR #578) is landed on `main`: `std/web` has `WebError`, `Handler` (`(request: Request) => Task<Result<Response, WebError>>`) and the request and response helpers. This step adds the application: the middleware chain, the `web { }` builder, the adapter to `std/http`'s handler and `app.answer(request)`. No `WebTest` yet (step 3), no example (step 4). ## Source - [WEB.md sections 3.1, 3.3, 3.8, 3.9](https://git.torb.dev/torbscript/language/src/branch/main/docs/design/WEB.md): the application, the middleware trait and `Next`, the error pages, `answer`. - [WEB.md section 8](https://git.torb.dev/torbscript/language/src/branch/main/docs/design/WEB.md), "One question for the checker": a bare function value of invisible captures may not cross into a task, a record carrying one may (probe 8). - `std/http` `Handler`, `Server` (`std/http/src/server.trb`), `docs/language/configuration/builders.md` for the builder shape. - Decision record: comment on #94 (the loop's decisions, incl. `assets` not in the vocabulary yet and the fallback for `Next`). ## Design - `trait Middleware { fn around(request: Request, next: Next): Task<Result<Response, WebError>> }` and `type Next { layers: List<Middleware>, endpoint: Handler, fn run(request) }` as in WEB.md section 3.3: `run` calls the first layer with a `Next` over the rest, or the endpoint when none is left. A middleware that answers without calling `next` refuses the request; one that changes the request builds a new `Request` from the parts of the old one and takes its body over (`Request`, `Response` and `Body` are shared types that own a one-shot stream, see the "Built" note in WEB.md 3.4). - `WebApp` is the builder receiver with exactly three members in this step: `wrap(layer: Middleware)` (layers run in the order written, the first outermost), `handle(handler: Handler)`, `errors(render: (WebError) => Response)`; no `assets` (#100). `web { }` follows the builder rules of `builders.md`; a block without `handle` is a compile-time or startup failure with a clear message, a second `handle` is refused the same way. Name choice stays inside the receiver-before-module rule (probe 7); the receiver has no other members. - `errors` is optional; the default renders `WebError.response()` of step 1. The hook sees the status and public message only, never the cause (it is logged by the adapter, never shown, WEB.md 3.8). - `app.answer(request): Task<Result<Response, WebError>>` runs layers, handler and, on a `Fail`, the `errors` renderer, so for a caller it answers `Ok(page)` for every `WebError`; it fails only when the renderer itself fails. (The WEB.md 3.9 sample matches both arms; the docs say which one `answer` takes.) - The adapter: `app.serving(): http.Handler` (name may follow the repo's participle rule) wraps `answer` into `std/http`'s `Handler` (`Result<Response, HttpError>`), so `Server.bind(...)` takes it unchanged; a `WebError` never reaches the transport as an `HttpError` because `answer` already rendered it. - **The probe.** First write `tests/conformance/web-next-record-capture.trb` (record `Next` carrying `endpoint: Handler` with invisible captures, awaited inside a task) and run it in the forge CI. If it checks: keep `Next` as in WEB.md and record "intended, held by this test" in WEB.md section 8. If the checker refuses it: `endpoint` becomes a trait value (`trait Endpoint { fn serve(request): Task<...> }` plus an adapter from a `Handler` closure written at the call site), WEB.md section 3.3 gets a "Built" note, and the probe stays as a negative conformance test with the expected error. Either way the loop records the result in the PR. - Touches the seed? No: new members of an existing package, no natives, no std name the compiler looks up by string. If the probe shows a checker gap, the fix is a separate issue, not part of this step. ## Acceptance criteria - [ ] `std/web/src/middleware.trb` (`Middleware`, `Next`) and `std/web/src/app.trb` (`WebApp`, `web`, `answer`, the `std/http` adapter) exported from `lib.trb`; every public item has a doc comment with an example. - [ ] `tests/conformance/web-next-record-capture.trb` exists and its outcome (checks, or refused with the expected error) is recorded in WEB.md section 8 and in the PR body. - [ ] Tests in `std/web/tests/app.test.trb` (handlers are `fn`s answering a `Task` awaited at the test file's top level, F13): two layers run outermost first and unwind in reverse; a layer that does not call `next` stops the chain and the handler is not run; a layer that rebuilds the request is seen below it and not above; a `Fail WebError.notFound()` from the handler answers `Ok` with status 404 and the default page; a custom `errors` renderer is used for 404 and 400; the cause is not in the rendered page; `web { }` without `handle` and with two `handle`s is refused. - [ ] A test serves `app.serving()` through `std/http`'s server on a loopback port (or the repo's existing in-memory transport used by `std/http` tests) and gets the 404 page and a 200 page back. - [ ] `docs/standard-library/web.md` gains the middleware, `web { }` and `answer` sections; `skills/` regenerated (`torb docs skill docs skills`); WEB.md status line and slice table say slice 2 is built up to `answer` (WebTest and the example remain). - [ ] Lands as at most two Conventional Commits: `feat(std/web): add Middleware and Next` (with the probe), `feat(std/web): add the web builder and the std/http adapter`. ## Seed No. New package members, no natives, no breaking change to a published contract. ## Size One PR, two commits, `std/web` only plus the probe, WEB.md and the docs page; about 1 h of implementation on top of #575. Sonnet/medium per the loop contract for `std/`; if the probe forces the trait-value fallback it is still the same PR. ## Dependencies - Needs: #575 (closed, landed on `main`), `std/http` `Handler`/`Server` (on `main`). - Unblocks: step 3 of #94 (`WebTest`), #97 (forms and CSRF), #98 (sessions and cookies), #100 (assets), #103 (`std/live`). Part of #94.
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261114 until 2026-11-14T06:30:00Z

[loop:torbscript] claim implement by session:loop-torbscript-20261114 until 2026-11-14T06:30:00Z
Author
Collaborator

[loop:torbscript] checkpoint: worktree loop/776-web-middleware-app, commit 1 (Middleware, Next, probe) pushed, PR #777 open as WIP. No local toolchain runs here (torb needs a newer glibc), so CI is the first check. next: read the probe result, then commit 2 (WebApp, web builder, adapter, docs).

[loop:torbscript] checkpoint: worktree loop/776-web-middleware-app, commit 1 (Middleware, Next, probe) pushed, PR #777 open as WIP. No local toolchain runs here (torb needs a newer glibc), so CI is the first check. next: read the probe result, then commit 2 (WebApp, web builder, adapter, docs).
Author
Collaborator

[loop:torbscript] checkpoint: PR #777 has both commits pushed (Middleware/Next + probe; WebApp, web builder, adapter, tests, docs, skills mirror, WEB.md). CI is queued (forge runners backed up, many runs waiting), no result yet. Nothing could run locally (torb needs a newer glibc). next: read CI, fix what it finds.

[loop:torbscript] checkpoint: PR #777 has both commits pushed (Middleware/Next + probe; WebApp, web builder, adapter, tests, docs, skills mirror, WEB.md). CI is queued (forge runners backed up, many runs waiting), no result yet. Nothing could run locally (torb needs a newer glibc). next: read CI, fix what it finds.
Author
Collaborator

[loop:torbscript] implement: claim released (worker run timed out at the 1 h lease). PR #777 has both commits pushed, CI queued behind the runner backlog, no result yet. next: read CI on PR #777, then review (argus).

[loop:torbscript] implement: claim released (worker run timed out at the 1 h lease). PR #777 has both commits pushed, CI queued behind the runner backlog, no result yet. next: read CI on PR #777, then review (argus).
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261114 until 2026-11-14T10:40Z

[loop:torbscript] claim implement by session:loop-torbscript-20261114 until 2026-11-14T10:40Z
Author
Collaborator

[loop:torbscript] review-fix: new head e7b46cd4. Cause: tier A and gates ran red on 7f61a966 for six gates: the doc examples of WebApp, web, tryWeb and Next were not self-contained (doc --check --no-run std), Middleware.around, Next.layers, Next.endpoint and Next.run had no doc comment, docs/standard-library/web.md and its skills mirror were stale (docs declarations --check, docs skill --check), two files were not formatted (std/web/tests/app.test.trb, tests/conformance/web-next-record-capture.trb), and the loopback test called a task's result inside a task (panic runToEnd() was called inside a task, native and VM). Fix (one fix commit on top, pushed to the branch): self-contained examples, doc comments, overLoopback now returns Task<String> and is awaited at the top level, formatted, docs and skills regenerated. Verified locally with the nightly torb on e7b46cd4: torb format --check std/web tests/conformance (0 of 394 would change), torb check . (1188 files, no problems), torb docs declarations --check, torb docs skill docs skills --check, torb docs index --check (all up to date), torb doc --check --no-run std/web (16 examples, no problems), torb test std/web on the VM (58 passed, 0 failed), conformance probe web-next-record-capture matches its .expected, tools/check-commits.sh origin/main HEAD clean. Not run locally: native tests and the full std package suites (no C compiler here), docs check (needs one). next: wait for CI, then review (argus)

[loop:torbscript] review-fix: new head e7b46cd4. Cause: tier A and gates ran red on 7f61a966 for six gates: the doc examples of WebApp, web, tryWeb and Next were not self-contained (`doc --check --no-run std`), Middleware.around, Next.layers, Next.endpoint and Next.run had no doc comment, docs/standard-library/web.md and its skills mirror were stale (`docs declarations --check`, `docs skill --check`), two files were not formatted (std/web/tests/app.test.trb, tests/conformance/web-next-record-capture.trb), and the loopback test called a task's result inside a task (panic `runToEnd() was called inside a task`, native and VM). Fix (one fix commit on top, pushed to the branch): self-contained examples, doc comments, `overLoopback` now returns `Task<String>` and is awaited at the top level, formatted, docs and skills regenerated. Verified locally with the nightly torb on e7b46cd4: `torb format --check std/web tests/conformance` (0 of 394 would change), `torb check .` (1188 files, no problems), `torb docs declarations --check`, `torb docs skill docs skills --check`, `torb docs index --check` (all up to date), `torb doc --check --no-run std/web` (16 examples, no problems), `torb test std/web` on the VM (58 passed, 0 failed), conformance probe web-next-record-capture matches its .expected, `tools/check-commits.sh origin/main HEAD` clean. Not run locally: native tests and the full std package suites (no C compiler here), `docs check` (needs one). next: wait for CI, then review (argus)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#776
No description provided.