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"