ADR-018: The Handler Sees the Request, Because the Lambda Already Could

Status: Superseded by ADR-289 — its D1-D5 are D17-D21 there Related: ADR-008 (views and what tethers them), ADR-011 D4 (a switch chooses the executor and nothing else), ADR-017 (escaping is the template’s contract)


1. Context

Part III 17.1 shows a server as

http::Server::new()
    .route("/") fn { "Hello World" }
    .listen(":8080")

and that is the whole of the handler’s signature: it takes nothing. fortunes.nika did not notice, because the fortunes benchmark needs neither a query parameter nor a status code — which is exactly how the gap survived. The other six TechEmpower tests need both, and so does every program anyone would write.

The question is not whether a handler sees the request. It is what shape that takes in a language whose lambdas already have a rule about arguments.


2. Decisions

D1 — The request is the handler’s first argument, under the rule the language already has.

A block lambda takes as many implicit arguments as its body reaches for (Part I, 5.3). Applied here, that is the whole decision:

.route("/")        fn { "Hello World" }                  // mentions none, takes none
.route("/hello")   fn { "Hello, {a.query("name") ?? "world"}" }
.route("/fortunes") fn(request) { render(request) }      // or name it

Nothing is added to the language and the specification’s own example keeps working, unchanged and for the same reason it worked before. A handler that does not mention the request does not receive it; one that does, does.

a is the request. There is no second implicit argument: a response is returned (D2), not filled in.

D2 — A handler returns something that answers the request, and std says which types do.

returns becomes
String 200, text/plain; charset=utf-8
html::Raw 200, text/html; charset=utf-8
Response itself
T throws the value on success; on failure 500 with a generic body, the error logged

The last row is a decision and not an omission. An error’s message is written for the operator, and a handler that returns one to the client is how internal paths, query fragments and driver messages end up in a bug bounty report. The error goes to the log with its context; the client gets a status.

html::Raw earning text/html is not a convenience: it is ADR-017 D2 read from the other end. The type that says “this is already markup” is the type that may be sent as markup, and a String — which might be anything — may not be.

D3 — A status code, a header or a body of one’s own is a Response, built where it is returned.

fn {
    if a.query("id").is_none() {
        http::Response(status: 400, body: "id is required")
    } else {
        lookup(a.query("id")??)
    }
}

The anonymous-constructor form of Kap 4.2, with named fields, and no builder chain: a response is a value, and the shape a program most often needs — a status and a body — is one line without ceremony. Headers are a field like the others.

D4 — The request’s strings are views, and the request owns the buffer.

request.path(), request.header("host") and request.query("name") yield &str into the bytes the connection read. This is ADR-008 applied without a new rule: a handler that uses a parameter inside the request’s own scope pays nothing, and one that stores it past the response has the compiler tell it to own the value first. At the request rates this specification argues about, one allocation per parameter is not a rounding error.

The surface is deliberately small, because each entry is a promise:

pub fn method(&self) -> Method               // an enum (Kap 4.4), not a string
pub fn path(&self) -> &str
pub fn query(&self, name: &str) -> &str?     // Kap 3.5's nullable, not an empty string
pub fn header(&self, name: &str) -> &str?    // case-insensitive, as the protocol is
pub fn body(&self) -> &[u8]                  // bytes; what they mean is the handler's business

What is not here: no params map that a route pattern fills in (route patterns are their own decision), no cookie parsing, no multipart. Each is a specification of its own and none of them is what G6 was about.

D5 — Neither build switch enters, and neither does async.

The same handler runs at every setting; a switch chooses the executor and nothing else (ADR-011 D4). A handler does I/O — that is what it is for — so it is not sync (Part II, 12.1), and it carries no async marker and no await, which is Part II 11.1 and the reason std::fs reads the way it does.


3. Consequences

For fortunes.nika. Its [G6] note becomes a reference to this ADR, and fortunes keeps the signature it had: it is the handler that needs nothing from the request, which is what made it a poor test of this corner in the first place.

For the implementation. Nothing here can be built before the runtime binding (tokio), which is the roadmap line after the bootstrap compiler. This ADR is therefore a decision and not a delivery, and says so in its status: what a handler is no longer waits on that work.

For the six other TechEmpower tests. plaintext and json need D2’s return types, db and queries need D3’s status codes, and updates needs D4’s query parameters. None of them now needs a language change to be written.


4. What was rejected

This site is open source. Improve this page.