Standard library / http · on the one-page reference
http
Experimental — new, manually tested rather than conformance-suite covered.
A one-shot HTTP client, the practical core of
Node's http
module. Not a 1:1 port: Node's classes
(ClientRequest/IncomingMessage/Agent)
are all built on EventEmitter and streams together. Custom
request headers and response headers (server-side) both work, via
Map<string, string>; response headers on the client
side are the one piece still deferred (the underlying HTTP client's
convenience wrapper only surfaces the status code -- reading real
headers needs a lower-level request flow than what's wired up yet).
~1.5× faster than Node's native
http client against a local server: 300 sequential
GETs, compiled with --release-fast, measured around 110ms
versus Node 20's ~160ms for the equivalent loop. Only skips work Node's
own client also skips for a plain (non-TLS) request — loading the
system CA bundle means reading and parsing a real certificate file from
disk, so it's only done for https:// URLs, not
unconditionally on every call.
http.request(url, method, body, headers) Wasm: limited
(string, string, string, Map<string, string>) →
{ status, body, ok, headers } — pass
"" for body on methods that don't take one,
and an empty Map for headers if there are
none to send. ok is status >= 200 and status
< 300, computed once here so every caller doesn't have to
write that range check themselves. status is
-1 if the request could not even connect. The response's
own headers is currently always an empty Map
— reading real response headers needs a lower-level request
flow than what's wired up yet, a deliberate, documented gap, not a
silent one.
const headers = new Map<string, string>();
headers.set("X-Request-Id", "abc123");
const r = http.request("https://postman-echo.com/post", "POST", "hello=world", headers);
console.log(r.status); // 200
console.log(r.ok); // true
http.get(url) Wasm: limited
string → { status, body, ok, headers }
— http.request(url, "GET", "", ...) with an empty
headers map.
const r = http.get("https://example.com");
console.log(r.status); // 200
console.log(r.body.includes("Example Domain")); // true
http.createServer(port, handler)
(int, (HttpRequest) → HttpResponse) → void —
accepts connections forever, never returns. handler must
be a named function (block-bodied inline arrow functions don't parse
in this position yet), and returns the same { status,
body, ok, headers } record the client side uses (ok
goes unused server-side; headers is written into the real
response, one line per entry). Superseded the old canned-response
serve() global, which always returned the same body to
every request; this one genuinely inspects each request.
function handleRequest(req: HttpRequest): HttpResponse {
const headers = new Map<string, string>();
headers.set("X-Powered-By", "lumen");
if (req.path == "/notfound") {
return { status: 404, body: "not found", ok: false, headers: headers };
}
return { status: 200, body: "hello from lumen (" + req.method + ")", ok: true, headers: headers };
}
http.createServer(8080, handleRequest);
Concurrent: each accepted connection's
full handling (including its keep-alive requests) now runs on a
worker thread from a dedicated thread pool, so accept()
loops back for the next connection immediately instead of waiting for
earlier ones to finish -- a genuinely blocking or slow handler on one
connection no longer stalls every other client. Verified against a
real compiled binary: three concurrent requests to a handler doing
~5s of real CPU work each completed in ~5.1s total (not ~15s), and a
trivial request fired mid-flight against a slow one returned in under
a millisecond. A handler now genuinely runs on multiple OS
threads at once — a handler that reads or mutates
shared global state without its own synchronization has a real data
race, unlike Node's single-threaded http.createServer;
Lumen has no general-purpose locking primitive yet, so this is a
documented trade-off, not a solved problem. Still no idle-connection
timeout: a connection stays open as long as the client keeps sending
requests. (Under --wasm: unchanged from before, still a
single-connection-at-a-time loop -- wasm32-wasi has no real OS
threads.)
~1.1–1.2× faster than Node's native
http.createServer on 300 sequential GETs (same
Node.js client hitting both servers, isolating the server as the only
variable): ~139ms vs Node's ~157ms. Wasn't always true — the
first version closed the connection after every response, forcing a
fresh TCP handshake per request and running ~1.3–1.5×
slower than Node; this server now supports HTTP keep-alive
(the connection stays open and serves further requests until the
client sends Connection: close), the same thing Node's
server does by default.
http.stream(url, method, body, headers)
The same four arguments as http.request, but instead of
waiting for the whole body it hands back a live handle to the connection — so a
response that arrives a line at a time can be read a line at a time, and a connection
that has stopped being HTTP can go on being used.
const s = http.stream("https://api.example.com/v1/chat", "POST", body, headers);
if (s.status() == 200) {
while (!s.done()) {
const line = s.readLine();
if (line != "") console.log(line);
}
}
s.close();
status() is the response status and header(name)
looks a response header up without regard to case. readLine() blocks for the
next decoded line of the body, done() reports exhaustion, and
close() drops the connection early.
Past a 101, it is a raw two-way pipe.
write(chunk) puts raw bytes on the connection and read() takes
one undelimited read off it — which is what a WebSocket needs and what
readLine() cannot do, since a binary frame carries no newline guarantee: a
frame without one would never return, and a frame that happened to contain one would come
back truncated.
This is also the only way to speak wss:// today.
net.connect has no TLS, and http.stream already owns a
TLS connection that keeps reading past a 101 Switching Protocols, so the
upgrade rides on the connection the client already made.
http.METHODS()
() → string[] — the 35 HTTP method
name strings ("ACL", "BIND", ...,
"UNSUBSCRIBE"), alphabetically sorted, matching Node's own
http.METHODS value exactly. Called as a zero-arg function
rather than read as a property, the same shape Math.PI()/
os.EOL() already use — Lumen has no static namespace
property access.
const methods = http.METHODS();
console.log(methods.length); // 35
http.STATUS_CODES()
() → Map<int, string> — every
standard HTTP status code (100 through 511) mapped to its reason
phrase, verbatim from Node's own STATUS_CODES table.
const codes = http.STATUS_CODES();
console.log(codes.get(404) ?? "?"); // "Not Found"