std Lends the Socket, the http Package Speaks the Protocol, and a Handler Is a Lambda Over the RequestStatus: Accepted
Date: October 3, 2026
History: the records this one replaces are archived in docs/history/adr; read them only for how the decision was reached
Answers: what an HTTP handler is given and what it gives back (G6 in
fortunes.nika); how a response reaches the socket when it is a file, and
whether the program has to read it; whether http is in std at all
(open-decisions.md’s what is std::http); what a program writes to be a
microservice and who decides what is exposed (open-decisions.md); what
std::net is (Part III 17.1’s one line, and
#90’s blocker); whether
nikaia serve is still coming; and whether a route may be refused, or listed,
by what its handler touches (open-decisions.md, decided by the owner)
Related:
ADR-303 D1, D2, D3, D5, D6 (the server is Nikaia’s own,
TLS is rustls, I/O split by kind, the runtime file, HTTP/1.1 first, and the
order of the work),
ADR-303 D8-D12 (a readiness wait is a registration — the layer every
socket wait here sits on),
ADR-121 D1 and D4 (the bell, and readiness as something awaitable),
ADR-290 (how the HTTP/1.1 parser becomes Nikaia: std::http1),
ADR-108 D1-D4 (a path names its root at the call; Anywhere is
listed by --trust),
ADR-010 D1, D2 and D8 (polarity, the provenance column, untrusted
paths),
ADR-017 D2 (html::Raw is the type of markup),
ADR-283, D19 above all (views, and the one shared
Bytes buffer),
ADR-292 D3 (the touches column and what it orders),
ADR-281 D30 (no alias analysis a compilation depends on),
ADR-297 D6 (the drain),
ADR-037 D2 (one thread for user code at no),
ADR-140 D2 (a type is constructed by its anonymous constructor),
ADR-203 D1 (an enum’s cases are in the ledger, so a match on
Method is complete),
ADR-286 D15 (pub is a package word; one path is one package)
What does a Nikaia program write to serve HTTP, and which part of that is the
language’s, which std’s and which a package’s?
| Language / tool | Its answer | What it costs there | Source |
|---|---|---|---|
Python, python -m http.server |
a standard file server, one command, binds every interface | a footgun by default; it serves a directory and demonstrates nothing about an application | D12 |
Rust, hyper on tokio |
a finished server over a readiness runtime | a reader picks a runtime before binding a socket; async/await in every signature |
ADR-303 D1, D10 |
| Apache httpd | EnableSendfile off by default (it was on in 2.2); EnableMMAP warns of a bus error when NFS truncates a mapped file |
the mechanism moved toward off for correctness, not speed | D24, D30; docs/history/zero-copy-send.md §5 |
| nginx | sendfile off by default; sendfile_max_chunk for fairness; SF_NODISKIO and a thread pool against a cold page cache |
a blocking sendfile occupies a worker |
D24, D30 |
| HAProxy, lighttpd | kernel splicing opt-in with a copy as fallback; server.network-backend names the mechanism as configuration |
— | D24, D25 |
What every server above converged on, independently: the application
declares what answers, the server chooses how it travels, the operator may
overrule. What does not transfer is the runtime choice: a Nikaia program
binds a socket without naming async, await, epoll or poll
(D10), which no language in the table offers.
| Option | Rejected because | Was |
|---|---|---|
http as a module of std |
moving a module out of std later breaks every use std::http; moving a package in is an addition. Nothing in it needs the compiler |
D2, D3 |
a standard file server, nikaia serve [dir] |
nothing waits on it, the people who want one have one, and it demonstrates nothing of the language (no handler, routing, ledger or provenance) | D12 |
| the operator names the routes for an application | an application’s author decided already; a tool that overrode them removes the reason to write it | D12 |
every pub fn is a route; an @route word |
pub is a package word; giving it the network may call it makes the dangerous meaning invisible (ADR-010 D1). No word is needed: the chain runs across a package boundary |
D14 |
a hidden current_request() |
thread-local state is a lie under work stealing and untestable | D17 |
fn(request, response) |
a mutable response makes was it sent? every layer’s question | D17 |
a Request argument always required |
breaks fn { "Hello World" }, which the implicit-argument rule already answers |
D17 |
a returned String served as HTML |
every string becomes a potential injection | D18 |
a reusable read buffer for net::read |
a second shape with its own lifetime story; what read returns may be kept |
D7 |
a non-blocking connect |
a second readiness shape for a case nothing has | D10 |
mmap per request |
2–3× worse than reading below a megabyte, in every run | D24 |
IORING_OP_SPLICE through a pipe |
52–58 µs machine CPU at 4 KiB against 9–45 for every other vehicle | D25 |
sendfile named by the program |
the mechanism is std’s and the operator’s, as in every server of §2 |
D24 |
refuse a route by what its handler touches (option 2) |
would mostly refuse correct programs, on a granularity the column does not have | D31 |
list every route’s touches in --trust (option 3) |
almost every route reaches something; a report long by construction is read once, and its filter is undesigned | D32, D33 |
The numbers that decided things, one line each:
mmap per request: 2–3× worse than reading, below a megabyte (D24).splice through a pipe: 52–58 µs of machine busy time at 4 KiB, worst in
its family, while its sending thread shows ~21 µs — machine busy time is the
column that answers (D25).http is a package, not a module of std, and it lives in examples/ until it is ripe.It is reached by a path, http = { path = "../http" }, the arm the build
already resolves. examples/ rather than a repository of its own, because there
it is compiled by the test that compiles every example and grows against a
moving compiler.
std.What needs the compiler is already in std under its own name: an untrusted
path (ADR-010 D8, and ADR-108’s root at the call),
escaping by type (std::html), a body the program never allocated (fs::map,
Bytes). What is left — the protocol and the framework — needs no compiler,
is large and ages.
A package moved into std later is an addition; a module moved out is a break.
So it starts outside, even for a reader who thinks a server belongs in a
standard library.
A version of std is a version of the language; a protocol wants to change its
surface twice and settle. Outside, that is two versions of a package. Ripeness
is its surface has stopped moving, not it is finished.
A path dependency needs no registry. The day http leaves examples/ is D4’s
ripeness, and a later decision.
std’s and the protocol is the package’s.D2’s subtraction carried one item further: a socket is an operating-system
resource of the kind fs and io own, and a Nikaia package cannot make a
syscall. std lends the socket; the package speaks HTTP. (The text half of
HTTP/1.1 — where a head ends, what its lines say — is std::http1, reaching no
socket, by ADR-290’s route; the protocol’s meaning stays the
package’s.)
std::netnet::listen(address) -> Listener // bind and listen
net::connect(address) -> Connection // the other end, for a client
Listener.accept() -> Connection // the next one, waiting
Listener.address() -> String // what port 0 turned into
Connection.read() -> Bytes // what the peer sent
Connection.write(bytes) // all of it
Connection.peer() -> String // who
Connection.close() // now rather than at the drop
read returns Bytes, ADR-283 D19’s one shared buffer, because
what comes back may be kept. write takes all of it; the loop a partial write
would need is inside. An empty read is the peer closing.
untrusted, and it is the first such source in std.net::Connection::read carries provenance = "untrusted" in the ledger;
ADR-010 D2’s analysis reads it unchanged.
touches gains socket, and it names no socket.A connection is what a program touches, and telling two apart is the alias analysis ADR-281 D30 refuses. Two reads never conflict; a write orders against every socket touch.
accept, read and write are non-blocking underneath and await
ADR-121 D4’s readiness (served by ADR-303’s
registrations), so one thread serves many connections at user_parallelism = no
exactly as at yes. Nothing in a .nika file that binds a socket says async,
await, epoll, io_uring or poll (ADR-303 D3). bind
answers immediately and connect blocks — stated, not hidden.
Cleanup is not waited for.close names the moment where a program wants one.
nikaia serve is cut.An application that serves has an author who decided already, and everything
about it — configuration, port, what it hard-codes — is the programmer’s;
nothing is the compiler’s business and no tool overrides it. There are two
products, each right about itself stands as the reason; the other product, a
standard file server nikaia serve [dir] whose every setting would have been
the operator’s, is cut — for schedule (nothing waits on it) and for value
(it would show the part of the language it has least of). A cut is written
down so that no list reads it as not yet.
Changed by ADR-326 D2, D5-D7: there is no connection cap; admission is a strategy of net::serve, and connections is the number after which the server returns; head_wait gives way to head_timeout and body_bytes_per_second.
body_cap, head_cap, head_wait and connections are options on
http::listen, with defaults that are a server and not a denial of service, and
a program names only what it changes. A body cap and a connection cap exist
from the first commit. http::listen(at) takes the address and there is no
default to make safe: the localhost default was fail-closed for a product whose
author was absent. Binding past localhost is the kind of thing
ADR-108’s shape lists — recorded per site, listed by
nikaia --trust — and that listing belongs there, not to either product.
@route and never every pub fn.Made precise by ADR-332: what a handler declares is all it receives.
Routing is the package’s own form, .route(…) with a handler, and
needs no language change. No word on the declaration, and pub is never read
as the network may call it. A type is constructed as http::Server(), not
http::Server::new() (ADR-140 D2).
Changed by ADR-290 D17: HTTP/1.1’s text half is written in Nikaia, std::http1, and reached from Rust as an ordinary module. Changed by ADR-326 D9: connections are kept alive.
GET and POST, bodies by Content-Length, Connection: close, no chunked
transfer and no TLS — ADR-303’s order read as a scope. The
parser is std::http1’s grammar (D6); the protocol’s meaning stays the
package’s.
examples/hello-http/ is the demonstration, and it is a real server.One address and one function: http::listen(at) fn(request) { … }, the
handler deciding, driven by a test over a real socket — port 0, the bound
address read back, both methods, a body by Content-Length, and each refusal.
A block lambda takes the arguments its body names (Part I 5.3), so
fn { "Hello World" } takes none and fn(request) { … } takes the request.
There is no second argument: a response is returned, not filled in.
| returns | becomes |
|---|---|
String |
200, text/plain; charset=utf-8 |
html::Raw |
200, text/html; charset=utf-8 |
Bytes |
200, application/octet-stream (D22) |
Response |
itself |
T throws |
the value on success; on failure 500 with a generic body, the error logged |
An error’s message is the operator’s and never reaches the client.
html::Raw earning text/html is ADR-017 D2 read from the other
end; a String may not be sent as markup.
Response, a value built where it is returned.Named fields, no builder chain; headers are a field like the others.
path(), header(name) and query(name) are views into the bytes the
connection read (ADR-283); one kept past the response is owned
first. The surface is small because each entry is a promise: method() an
enum, path(), query(name) and header(name) nullable rather than empty,
header case-insensitive, body() the bytes. No params map, cookies or
multipart.
async.The same handler runs at every setting (ADR-296 D19). It does I/O,
so it is not sync, and it carries no async and no await.
Bytes, and a Mapped that derefs to it, are bodies (D18’s third row). A page
mapped once outside the handler costs a reference count per request. A handler
answering with a captured buffer duplicates the handle, since it is called
many times (ADR-312 D1).
http::File.http::File is a response body, not a file handle; std::fs gains no handle
type. Its Content-Type follows the extension, else application/octet-stream,
and a Response header overrides it. The escaping contract is untouched.
std’s choice, and what it may keep decides more than the mechanism.std chooses at run time between sendfile(2), a mapping written out and a
plain read; std may not map per request; an operator may overrule it with
a send-file key in ADR-303 D5’s runtime file, default auto,
pinnable to never or a size. No crossover size is named.
splice on the ring is not the mechanism.A file cannot be spliced to a socket without a pipe between them, which is a second transfer. The column that answers is the machine’s busy time, not the sending thread’s.
Bytes that never enter the process cannot be encrypted, and HTTP/2 interleaves
DATA frames; the file is mapped and written through the session. On
wasm32-unknown it is read. kTLS is not decided (§6).
http::File stats the file when the response is built, which gives
Content-Length and turns a missing file into D18’s 500. A failure mid-body
closes the connection short and is logged.
http::File takes its root like every path-taking std function, and a name that leaves it is 404.http::File(path, root), an fs::Root with no default (ADR-108
D1-D3); fs::Outside is answered with 404 before the status line;
fs::Root::Anywhere is listed by --trust. It refuses, it does not sanitise,
and no provenance follows the name.
std keeps is bounded, invalidated, not a promise, and only under a named root.A table from resolved path to mapping, sized by static-cache in the runtime
file (0 disables it); entries dropped when the file’s identity or
modification time moves, with no key to turn that off; no hit is promised or
reported; nothing is kept for an Anywhere root. A page that must be kept is
mapped by the program (D22).
A mapping whose file another writer truncates faults the process, so std
keeps no mapping of a file it does not own the directory of. A sendfile on a
cold page cache blocks, and at user_parallelism = no that stops every
connection, so whatever serves D23 pauses through the executor — a file to a
socket reaches the I/O thread, not the handler’s (ADR-303 D3).
touchesNo reaching: at a route, no word, no NK code: touches stays a fact. A
refusal would mostly refuse correct programs, on a column whose path is a
parameter’s name and not a value.
nikaia --trust gains no section.In a real application almost every route reaches something, and a report whose rows nearly all fire tells a reviewer nothing. A useful listing would need a filter and a switch nobody has designed; refusing nothing costs nothing and stays extendable.
ADR-108 D4’s listing works because it lists ways around a
check and is short by construction; a list of what every handler touches is
long by construction. fs::Root’s listing is not option 3 arriving for the
filesystem.
async, no runtime choice and no word on a declaration. Nothing in
the language changed for any of it.std grew a socket, its first untrusted source and its first new
operating-system resource since fs; the protocol and the framework live in
examples/http/, where they may change twice without breaking the language.python -m http.server ergonomics of the original ask are not
answered, said plainly: that is the half given up.--trust keeps its two sections (provenance, and roots with nothing to
check against); touches gains no consumer.send-file and static-cache with the file
bodies (D24, D29).http leaves
examples/ (D4, D5).rustls, ADR-303 D2), HTTP/2
and kTLS, each a later step of ADR-303’s order.read the day a
measurement asks), a timeout parameter on net (a slow peer is the
server’s caps), and net on a target without an operating system (it
does not exist there, Part III 17.2).--extended, --including http or
another name), and the better-than-touches granularity
ADR-108’s root already gives the filesystem. A refusal stays
available on a finer granularity.