Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019// node:http server over the SW virtual origin. A request arrives from the kernel// as one HttpRequestRecord and becomes an IncomingMessage (a Readable) with a// VirtualSocket; the response is buffered and posted as one HttpResponseRecord on// end(). A raw connection (a page WebSocket) arrives as a MessagePort of bytes and// becomes a PortSocket: its request head is parsed here and handed to "upgrade".// No TCP, no response streaming, no client (DESIGN progress notes).import type { ConnectionData, HttpHeaders, HttpRequestRecord, HttpResponseRecord, SyscallSpec } from "../shared/protocol";import { rawHttpResponse } from "../shared/protocol";import { PortSocket } from "./socket";import type { Loop } from "./loop";import { capture, runInFrame, type Context } from "./async_hooks";import { EventEmitter, type EventEmitterConstructor } from "./events";import { Buffer } from "./buffer";import { invalidArgType } from "./errors";import { Readable, Stream } from "./stream";import { nextTick } from "./ticks";import { markRefused, refused, unsupported } from "./refuse";
const CLIENT_WHY = "the http client is not implemented yet";
/** Node's checkIsHttpToken / checkInvalidHeaderChar (lib/_http_common.js). */const TOKEN_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;const INVALID_HEADER_CHAR_RE = /[^\t\x20-\x7e\x80-\xff]/;
/** http.validateHeaderName: Node's own check, usable standalone (some libraries call * it before setHeader to fail early with Node's exact error). */export function validateHeaderName(name: unknown, label = "Header name"): void { if (typeof name !== "string" || !name || !TOKEN_RE.test(name)) { throw Object.assign(new TypeError(`${label} must be a valid HTTP token ["${String(name)}"]`), { code: "ERR_INVALID_HTTP_TOKEN" }); }}
/** http.validateHeaderValue: undefined is the only type that's rejected outright; * everything else is coerced to a string and checked for control/non-Latin1 bytes. */export function validateHeaderValue(name: unknown, value: unknown): void { if (value === undefined) { throw Object.assign(new TypeError(`Invalid value "${String(value)}" for header "${String(name)}"`), { code: "ERR_HTTP_INVALID_HEADER_VALUE" }); } if (INVALID_HEADER_CHAR_RE.test(String(value))) { throw Object.assign(new TypeError(`Invalid character in header content ["${String(name)}"]`), { code: "ERR_INVALID_CHAR" }); }}
// Node ships a stack-trace-free variant for its own hot paths (V8's// prepareStackTrace is expensive); ours has no such cost to dodge, so it's the same// function, kept only so code that reaches for `.withoutStackTrace` finds one.(validateHeaderName as typeof validateHeaderName & { withoutStackTrace: typeof validateHeaderName }).withoutStackTrace = validateHeaderName;(validateHeaderValue as typeof validateHeaderValue & { withoutStackTrace: typeof validateHeaderValue }).withoutStackTrace = validateHeaderValue;
type Listener = (...args: any[]) => void;
export const STATUS_CODES: Record<number, string> = { 100: "Continue", 101: "Switching Protocols", 102: "Processing", 103: "Early Hints", 200: "OK", 201: "Created", 202: "Accepted", 203: "Non-Authoritative Information", 204: "No Content", 205: "Reset Content", 206: "Partial Content", 207: "Multi-Status", 208: "Already Reported", 226: "IM Used", 300: "Multiple Choices", 301: "Moved Permanently", 302: "Found", 303: "See Other", 304: "Not Modified", 305: "Use Proxy", 307: "Temporary Redirect", 308: "Permanent Redirect", 400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 411: "Length Required", 412: "Precondition Failed", 413: "Payload Too Large", 414: "URI Too Long", 415: "Unsupported Media Type", 416: "Range Not Satisfiable", 417: "Expectation Failed", 418: "I'm a Teapot", 421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked", 424: "Failed Dependency", 425: "Too Early", 426: "Upgrade Required", 428: "Precondition Required", 429: "Too Many Requests", 431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported", 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected", 509: "Bandwidth Limit Exceeded", 510: "Not Extended", 511: "Network Authentication Required",};
export const METHODS = [ "ACL", "BIND", "CHECKOUT", "CONNECT", "COPY", "DELETE", "GET", "HEAD", "LINK", "LOCK", "M-SEARCH", "MERGE", "MKACTIVITY", "MKCALENDAR", "MKCOL", "MOVE", "NOTIFY", "OPTIONS", "PATCH", "POST", "PROPFIND", "PROPPATCH", "PURGE", "PUT", "QUERY", "REBIND", "REPORT", "SEARCH", "SOURCE", "SUBSCRIBE", "TRACE", "UNBIND", "UNLINK", "UNLOCK", "UNSUBSCRIBE",];
function nodeError(code: string, message: string, extra: Record<string, unknown> = {}): Error { return Object.assign(new Error(message), { code, ...extra });}
const enc = new TextEncoder();
function toBytes(chunk: unknown, encoding?: string): Uint8Array { if (typeof chunk === "string") return encoding ? Buffer.from(chunk, encoding) : enc.encode(chunk); if (chunk instanceof Uint8Array) return chunk; if (ArrayBuffer.isView(chunk)) return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength); throw nodeError("ERR_INVALID_ARG_TYPE", 'The "chunk" argument must be of type string or an instance of Buffer or Uint8Array.');}
let nextRemotePort = 49152;
/** * One per request: the honest stand-in for the TCP connection a Node server would * see. Requests reach the guest over the SW bridge, not a socket, so this carries * addressing facts and lifecycle (destroy ends the exchange) and no byte stream. */export class VirtualSocket extends EventEmitter { readonly encrypted = false; readonly remoteAddress = "127.0.0.1"; readonly remoteFamily = "IPv4"; readonly remotePort: number; readonly localAddress = "127.0.0.1"; readonly localPort: number; readonly server: unknown; #destroyed = false; readonly #onDestroy: (error?: Error) => void;
constructor(server: unknown, port: number, onDestroy: (error?: Error) => void) { super(); this.server = server; this.localPort = port; this.remotePort = nextRemotePort; nextRemotePort = nextRemotePort === 65535 ? 49152 : nextRemotePort + 1; this.#onDestroy = onDestroy; }
get destroyed(): boolean { return this.#destroyed; } get readable(): boolean { return !this.#destroyed; } get writable(): boolean { return !this.#destroyed; }
address(): { address: string; family: string; port: number } { return { address: this.localAddress, family: "IPv4", port: this.localPort }; }
destroy(error?: Error): this { if (this.#destroyed) return this; this.#destroyed = true; this.#onDestroy(error); nextTick(() => this.emit("close", error !== undefined)); return this; } /** The exchange has no pending writes to wait for, so these close at once. */ destroySoon(): void { this.destroy(); } end(): this { return this.destroy(); }
setTimeout(_ms: number, cb?: () => void): this { if (cb) this.once("timeout", cb); return this; } setNoDelay(): this { return this; } setKeepAlive(): this { return this; } ref(): this { return this; } unref(): this { return this; }}
/** What a request arrived on: a bridged exchange, or a raw connection. */export type ServerSocket = VirtualSocket | PortSocket;
export class IncomingMessage extends Readable { readonly method: string; readonly url: string; readonly headers: Record<string, string>; readonly headersDistinct: Record<string, string[]>; /** No chunked transfer here (the body arrives whole), so there are never trailers. */ readonly trailers: Record<string, string> = Object.create(null); readonly trailersDistinct: Record<string, string[]> = Object.create(null); readonly rawHeaders: string[]; readonly httpVersion = "1.1"; readonly httpVersionMajor = 1; readonly httpVersionMinor = 1; readonly socket: ServerSocket; complete = false; aborted = false; /** True once the request was handed to an upgrade listener. */ upgrade = false; #abort: AbortController | null = null;
/** The whole body is already here (buffered by the bridge): push it, then EOF. It * waits in the buffer until a consumer reads, so a late 'data' listener gets it. */ constructor(req: HttpRequestRecord, socket: ServerSocket) { super(); this.method = req.method; this.url = req.url; this.socket = socket; this.rawHeaders = req.headers.flat(); const headers: Record<string, string> = {}; const distinct: Record<string, string[]> = {}; for (const [raw, value] of req.headers) { const name = raw.toLowerCase(); headers[name] = Object.hasOwn(headers, name) ? `${headers[name]}, ${value}` : value; (distinct[name] ??= []).push(value); } this.headers = headers; this.headersDistinct = distinct; if (req.body?.length) this.push(new Buffer(req.body.buffer as ArrayBuffer, req.body.byteOffset, req.body.byteLength)); this.push(null); this.complete = true; }
get connection(): ServerSocket { return this.socket; }
/** Aborts once the message closes (destroyed, or the exchange ends abnormally), * built lazily since most guests never read it. */ get signal(): AbortSignal { if (!this.#abort) { this.#abort = new AbortController(); this.once("close", () => this.#abort!.abort()); } return this.#abort.signal; }
setTimeout(ms: number, cb?: () => void): this { this.socket.setTimeout(ms, cb); return this; }
override _read(): void {}
_dump(): void { if (this.readableEnded || this.destroyed) return; this.removeAllListeners("data"); this.resume(); }}
type HeaderValue = string | number | readonly string[];
/** Node's OutgoingMessage: the header bookkeeping and write/end mechanics shared by * ServerResponse and ClientRequest. Fields are `protected`, not private, so both * subclasses (and anything a guest subclasses from them) can reach in, as Node's own * lib/_http_outgoing.js does with plain properties. */export class OutgoingMessage extends (Stream as unknown as EventEmitterConstructor) { headersSent = false; writableEnded = false; protected _destroyed = false; protected readonly _headers = new Map<string, HeaderValue>(); // lowercased name → value protected readonly _chunks: Uint8Array[] = [];
/** Node's OutgoingMessage keeps `writable` true after end(); only destroy clears it. */ get writable(): boolean { return !this._destroyed; } /** end() hands the whole body to the bridge at once, so it is flushed when ended. */ get writableFinished(): boolean { return this.writableEnded; } get destroyed(): boolean { return this._destroyed; } /** Deprecated alias Node still ships (on-finished reads it). */ get finished(): boolean { return this.writableEnded; }
setHeader(name: string, value: HeaderValue): this { if (this.headersSent) throw nodeError("ERR_HTTP_HEADERS_SENT", "Cannot set headers after they are sent to the client"); this._headers.set(name.toLowerCase(), value); return this; } getHeader(name: string): HeaderValue | undefined { return this._headers.get(name.toLowerCase()); } getHeaders(): Record<string, HeaderValue> { return Object.assign(Object.create(null), Object.fromEntries(this._headers)); } getHeaderNames(): string[] { return [...this._headers.keys()]; } hasHeader(name: string): boolean { return this._headers.has(name.toLowerCase()); } removeHeader(name: string): void { if (this.headersSent) throw nodeError("ERR_HTTP_HEADERS_SENT", "Cannot remove headers after they are sent to the client"); this._headers.delete(name.toLowerCase()); }
/** Headers travel with the buffered body, so flushing only marks them sent. */ flushHeaders(): void { this.headersSent = true; }
/** Always true: bodies are buffered whole until end(), so there is no backpressure. */ write(chunk: unknown, encoding?: string | ((err?: Error) => void), cb?: (err?: Error) => void): boolean { const done = typeof encoding === "function" ? encoding : cb; if (this.writableEnded || this._destroyed) { const err = nodeError(this._destroyed ? "ERR_STREAM_DESTROYED" : "ERR_STREAM_WRITE_AFTER_END", this._destroyed ? "Cannot call write after a stream was destroyed" : "write after end"); nextTick(() => { done?.(err); if (!this._destroyed) this.emit("error", err); }); return false; } this._chunks.push(toBytes(chunk, typeof encoding === "string" ? encoding : undefined)); this.headersSent = true; if (done) nextTick(() => done()); return true; }
/** Marks the message ended and calls the `_final` hook; subclasses that actually * deliver the body (ServerResponse) override it. */ end(chunk?: unknown, encoding?: string | (() => void), cb?: () => void): this { const done = typeof chunk === "function" ? chunk as () => void : typeof encoding === "function" ? encoding : cb; if (this.writableEnded || this._destroyed) return this; if (chunk !== undefined && chunk !== null && typeof chunk !== "function") { this._chunks.push(toBytes(chunk, typeof encoding === "string" ? encoding : undefined)); } this.writableEnded = true; this.headersSent = true; this._final(); nextTick(() => { this.emit("finish"); done?.(); this._afterFinish(); }); return this; }
/** Called synchronously once end() has buffered the last chunk. No-op by default. */ protected _final(): void {} /** Called a tick after 'finish' and the end() callback. No-op by default. */ protected _afterFinish(): void {}
destroy(error?: Error): this { if (this._destroyed) return this; this._destroyed = true; nextTick(() => this.emit("close", error !== undefined)); return this; }
protected _headerList(): HttpHeaders { const headers: HttpHeaders = []; for (const [name, value] of this._headers) { if (Array.isArray(value)) for (const v of value) headers.push([name, String(v)]); else headers.push([name, String(value)]); } return headers; }
protected _body(): Uint8Array { let total = 0; for (const c of this._chunks) total += c.length; const body = new Uint8Array(total); let at = 0; for (const c of this._chunks) { body.set(c, at); at += c.length; } return body; }}
export class ServerResponse extends OutgoingMessage { statusCode = 200; statusMessage = ""; readonly req: IncomingMessage; readonly socket: ServerSocket; #closed = false; readonly #send: (res: HttpResponseRecord) => void;
constructor(req: IncomingMessage, send: (res: HttpResponseRecord) => void) { super(); this.req = req; this.socket = req.socket; this.#send = send; }
get connection(): ServerSocket { return this.socket; }
writeHead( status: number, message?: string | Record<string, HeaderValue> | HeaderValue[], headers?: Record<string, HeaderValue> | HeaderValue[], ): this { if (this.headersSent) throw nodeError("ERR_HTTP_HEADERS_SENT", "Cannot write headers after they are sent to the client"); if (!Number.isInteger(status) || status < 100 || status > 999) { throw nodeError("ERR_HTTP_INVALID_STATUS_CODE", `Invalid status code: ${status}`); } if (typeof message === "string") this.statusMessage = message; else headers = message; this.statusCode = status; if (Array.isArray(headers)) { // Node's flat form: [name, value, name, value, ...] for (let i = 0; i + 1 < headers.length; i += 2) this.setHeader(String(headers[i]), headers[i + 1] as HeaderValue); } else if (headers) { for (const [name, value] of Object.entries(headers)) this.setHeader(name, value); } this.headersSent = true; return this; }
/** 1xx informational responses have no way to reach the page: a Service Worker's * fetch handler delivers exactly one Response, and the Fetch spec drops any 1xx * before script ever sees it (there is no vehicle for a second, final response). * These stay no-ops rather than throwing: Node itself documents 1xx delivery as * best-effort, never guaranteed, so callers already treat it as fire-and-forget. * See the report for the Service-Worker-side limitation this papers over. */ writeContinue(cb?: () => void): void { if (cb) nextTick(cb); } writeProcessing(cb?: () => void): void { if (cb) nextTick(cb); } writeEarlyHints(hints: unknown, cb?: () => void): void { if (hints === null || typeof hints !== "object") throw invalidArgType("hints", "of type object", hints); if (cb) nextTick(cb); }
/** end() hands the whole buffered body to the bridge at once. */ protected override _final(): void { this.#send({ status: this.statusCode, headers: this._headerList(), body: this._body() }); } /** One request per connection: the exchange closes once the response is out. */ protected override _afterFinish(): void { this.socket.destroy(); }
/** Tears down the exchange; before end() the page gets a 502 instead of a response. */ override destroy(error?: Error): this { if (this._destroyed) return this; this._destroyed = true; this.socket.destroy(error); return this; }
/** The socket closed (response delivered or connection destroyed): 'close', once. */ _socketClosed(): void { if (this.#closed) return; this.#closed = true; nextTick(() => this.emit("close")); }}
/** http.ClientRequest: exported so imports and `instanceof` checks resolve (frameworks * branch on `req instanceof http.ClientRequest`), behind the same refusal as * http.request() — there is nothing to connect it to. */export class ClientRequest extends OutgoingMessage { constructor() { super(); throw unsupported("http", "ClientRequest", CLIENT_WHY); }}markRefused(ClientRequest, CLIENT_WHY);
interface AgentLikeSocket { destroy?(error?: Error): unknown; ref?(): unknown; unref?(): unknown; setKeepAlive?(on: boolean, ms: number): unknown; writable?: boolean;}
interface AgentLikeRequest { reusedSocket?: boolean; onSocket?(socket: unknown, error?: Error): void;}
/** http.Agent: the pooling bookkeeping Node's own Agent keeps — getName, the * sockets/freeSockets/requests tables, keepSocketAlive, reuseSocket, removeSocket, * destroy — minus a working createConnection, since the http client is not * implemented yet. Libraries subclass Agent and override createConnection with their * own transport; everything else here behaves as it does in Node for them. The * default createConnection throws, so routing an actual request through the base * Agent fails exactly where the client itself would. */export class Agent extends EventEmitter { static defaultMaxSockets = Infinity;
readonly options: Record<string, unknown>; readonly defaultPort = 80; readonly protocol = "http:"; /** Deprecated Node API, always null outside the (removed) domain module. */ readonly domain: null = null; readonly requests: Record<string, AgentLikeRequest[]> = Object.create(null); readonly sockets: Record<string, AgentLikeSocket[]> = Object.create(null); readonly freeSockets: Record<string, AgentLikeSocket[]> = Object.create(null); keepAliveMsecs: number; keepAlive: boolean; maxSockets: number; maxFreeSockets: number; scheduling: "fifo" | "lifo"; maxTotalSockets: number; totalSocketCount = 0; agentKeepAliveTimeoutBuffer: number;
constructor(options: Record<string, unknown> = {}) { super(); this.options = Object.assign(Object.create(null), options); if (this.options.noDelay === undefined) this.options.noDelay = true; // Node's own comment: don't confuse net and make it think we're connecting to a pipe. this.options.path = null; this.keepAliveMsecs = (options.keepAliveMsecs as number) || 1000; this.keepAlive = Boolean(options.keepAlive); this.maxSockets = (options.maxSockets as number) || Agent.defaultMaxSockets; this.maxFreeSockets = (options.maxFreeSockets as number) || 256; this.scheduling = options.scheduling === "fifo" ? "fifo" : "lifo"; this.maxTotalSockets = (options.maxTotalSockets as number) ?? Infinity; const buffer = options.agentKeepAliveTimeoutBuffer; this.agentKeepAliveTimeoutBuffer = typeof buffer === "number" && buffer >= 0 && Number.isFinite(buffer) ? buffer : 1000; }
/** Node's own algorithm (lib/_http_agent.js): a pooling key per host/port/local * address/family/socketPath, so two requests that would reuse the same real * connection land in the same bucket. */ getName(options: { host?: string; port?: number; localAddress?: string; family?: number; socketPath?: string } = {}): string { let name = options.host || "localhost"; name += ":"; if (options.port) name += options.port; name += ":"; if (options.localAddress) name += options.localAddress; if (options.family === 4 || options.family === 6) name += `:${options.family}`; if (options.socketPath) name += `:${options.socketPath}`; return name; }
/** Overridden by subclasses to supply a transport; the base Agent has none. */ createConnection(_options: unknown, _cb: (err: Error | null, socket?: AgentLikeSocket) => void): AgentLikeSocket | undefined { throw unsupported("http", "Agent#createConnection", CLIENT_WHY); }
createSocket(req: AgentLikeRequest, options: Record<string, unknown>, cb: (err: Error | null, socket?: AgentLikeSocket) => void): void { const name = this.getName(options); let called = false; const oncreate = (err: Error | null, socket?: AgentLikeSocket): void => { if (called) return; called = true; if (err) { cb(err); return; } (this.sockets[name] ??= []).push(socket!); this.totalSocketCount++; cb(null, socket); }; try { const socket = this.createConnection({ ...this.options, ...options }, oncreate); if (socket) oncreate(null, socket); } catch (err) { oncreate(err as Error); } }
addRequest(req: AgentLikeRequest, options: Record<string, unknown> = {}): void { const name = this.getName(options); const free = this.freeSockets[name]; if (free?.length) { const socket = (this.scheduling === "fifo" ? free.shift() : free.pop())!; if (!free.length) delete this.freeSockets[name]; this.reuseSocket(socket, req); (this.sockets[name] ??= []).push(socket); req.onSocket?.(socket); return; } const busy = this.sockets[name]?.length ?? 0; if (busy < this.maxSockets && this.totalSocketCount < this.maxTotalSockets) { this.createSocket(req, options, (err, socket) => req.onSocket?.(socket, err ?? undefined)); } else { (this.requests[name] ??= []).push(req); } }
keepSocketAlive(socket: AgentLikeSocket): boolean { socket.setKeepAlive?.(true, this.keepAliveMsecs); socket.unref?.(); return true; }
reuseSocket(socket: AgentLikeSocket, req: AgentLikeRequest): void { req.reusedSocket = true; socket.ref?.(); }
/** Node's own doesn't touch totalSocketCount here — that happens on the socket's * 'close', which this base Agent never wires up (there's no real socket to wire). * It only unbuckets the socket, from freeSockets too when it's no longer writable. */ removeSocket(socket: AgentLikeSocket, options: Record<string, unknown> = {}): void { const name = this.getName(options); const sets = socket.writable === false ? [this.sockets, this.freeSockets] : [this.sockets]; for (const set of sets) { const list = set[name]; if (!list) continue; const i = list.indexOf(socket); if (i !== -1) list.splice(i, 1); if (list.length === 0) delete set[name]; } }
destroy(): void { for (const set of [this.sockets, this.freeSockets]) { for (const name of Object.keys(set)) { for (const socket of set[name]!) socket.destroy?.(); delete set[name]; } } }}markRefused(Agent.prototype.createConnection, CLIENT_WHY);
/** http.globalAgent: a real instance (not a stand-in), so libraries that read its * pooling properties at load time see the same shape Node gives them. Node builds * its own globalAgent with these same options (and a `proxyEnv` key that's present * but unset unless NODE_USE_ENV_PROXY is on). */export const globalAgent = new Agent({ keepAlive: true, scheduling: "lifo", timeout: 5000, proxyEnv: undefined });
/** Node's default --max-http-header-size. */const MAX_HEADER_BYTES = 16 * 1024;/** http.maxHeaderSize: a getter in Node (it reads the live --max-http-header-size), * so it's exported the same way rather than a plain number a later edit could drift * from MAX_HEADER_BYTES, the limit the request-head parser below actually enforces. */export const maxHeaderSize = MAX_HEADER_BYTES;
function outOfRange(name: string, range: string, received: unknown): RangeError { return Object.assign(new RangeError(`The value of "${name}" is out of range. It must be ${range}. Received ${received}`), { code: "ERR_OUT_OF_RANGE" });}
/** http.setMaxIdleHTTPParsers: Node pools parser objects per idle keep-alive socket; * there is no parser pool here (no keep-alive, no persistent sockets), so this * validates like Node and otherwise has nothing to act on. */export function setMaxIdleHTTPParsers(max: unknown): void { if (typeof max !== "number") throw invalidArgType("max", "of type number", max); if (!Number.isInteger(max)) throw outOfRange("max", "an integer", max); if (max < 1 || max > Number.MAX_SAFE_INTEGER) throw outOfRange("max", `>= 1 && <= ${Number.MAX_SAFE_INTEGER}`, max);}
/** http.setGlobalProxyFromEnv: Node points the global Agent's connections at * HTTP_PROXY/HTTPS_PROXY/NO_PROXY. There is no client to route (CLIENT_WHY), so this * validates its argument like Node and then has nothing to set — accepted, not * refused, so code that calls it defensively on every platform doesn't need a * try/catch just for this one. */export function setGlobalProxyFromEnv(env: unknown = globalThis.process?.env): void { if (env === null || typeof env !== "object") throw invalidArgType("proxyEnv", "of type object", env);}
// What Node writes for a bad request head when no 'clientError' listener takes it.const CLIENT_ERROR_400 = enc.encode("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");const CLIENT_ERROR_431 = enc.encode("HTTP/1.1 431 Request Header Fields Too Large\r\nConnection: close\r\n\r\n");
function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array { if (a.length === 0) return b; const out = new Uint8Array(a.length + b.length); out.set(a); out.set(b, a.length); return out;}
/** Index of the CRLFCRLF that ends a request head, or -1. */function headEnd(bytes: Uint8Array): number { for (let i = 0; i + 3 < bytes.length; i++) { if (bytes[i] === 13 && bytes[i + 1] === 10 && bytes[i + 2] === 13 && bytes[i + 3] === 10) return i; } return -1;}
const TOKEN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
/** An HTTP/1.1 request line and header lines; null when malformed. */function parseRequestHead(bytes: Uint8Array): HttpRequestRecord | null { const [line, ...fields] = new TextDecoder("latin1").decode(bytes).split("\r\n"); const m = /^([A-Z-]+) (\S+) HTTP\/1\.[01]$/.exec(line ?? ""); if (!m) return null; const headers: HttpHeaders = []; for (const field of fields) { const colon = field.indexOf(":"); const name = field.slice(0, colon); if (colon <= 0 || !TOKEN.test(name)) return null; headers.push([name, field.slice(colon + 1).trim()]); } return { method: m[1]!, url: m[2]!, headers, body: null };}
/** Node's parser flags an upgrade when Connection lists "upgrade" and Upgrade is set. */function wantsUpgrade(req: IncomingMessage): boolean { const connection = req.headers.connection ?? ""; return req.headers.upgrade !== undefined && connection.split(",").some((t) => t.trim().toLowerCase() === "upgrade");}
/** A buffered response as HTTP/1.1 bytes for a raw connection, which then closes. */function serializeResponse(res: HttpResponseRecord, statusMessage: string): Uint8Array { const lines = [`HTTP/1.1 ${res.status} ${statusMessage || STATUS_CODES[res.status] || "unknown"}`]; const has = (header: string): boolean => res.headers.some(([name]) => name === header); for (const [name, value] of res.headers) lines.push(`${name}: ${value}`); if (!has("content-length") && !has("transfer-encoding")) lines.push(`content-length: ${res.body.length}`); if (!has("connection")) lines.push("connection: close"); return concatBytes(enc.encode(lines.join("\r\n") + "\r\n\r\n"), res.body);}
export interface HttpDeps { call<K extends keyof SyscallSpec>(op: K, args: SyscallSpec[K]["args"]): Promise<SyscallSpec[K]["ret"]>; loop: Loop; respond(id: number, res: HttpResponseRecord): void; /** A raw connection closed (both sides done, or destroyed). */ closed(id: number): void;}
export interface HttpRuntime { readonly module: { createServer(options?: object | Listener, handler?: Listener): Server; Server: new (options?: object | Listener, handler?: Listener) => Server; IncomingMessage: typeof IncomingMessage; ServerResponse: typeof ServerResponse; OutgoingMessage: typeof OutgoingMessage; ClientRequest: typeof ClientRequest; STATUS_CODES: typeof STATUS_CODES; METHODS: typeof METHODS; request: (...args: unknown[]) => never; get: (...args: unknown[]) => never; Agent: typeof Agent; globalAgent: Agent; validateHeaderName: typeof validateHeaderName; validateHeaderValue: typeof validateHeaderValue; maxHeaderSize: number; setMaxIdleHTTPParsers: (max: number) => void; setGlobalProxyFromEnv: (env?: Record<string, string | undefined>) => void; WebSocket: typeof WebSocket; CloseEvent: typeof CloseEvent; MessageEvent: typeof MessageEvent; }; dispatch(msg: { id: number; port: number; req: HttpRequestRecord }): void; connect(msg: { id: number; port: number; socket: MessagePort }): void;}
type ServerState = | { kind: "idle" } | { kind: "binding"; port: number } | { kind: "listening"; port: number } | { kind: "closed" };
export interface Server extends EventEmitter { readonly listening: boolean; listen(port: number, ...rest: unknown[]): this; close(cb?: (err?: Error) => void): this; address(): { address: string; family: string; port: number } | null; setTimeout(msecs: number, cb?: () => void): this; closeAllConnections(): void; closeIdleConnections(): void;}
export function createHttp(deps: HttpDeps): HttpRuntime { const { call, loop } = deps; const servers = new Map<number, ServerImpl>(); // this process's listening servers // listen() creates the handle (probed: the request handler, req's events, and // 'connection' all run in the frame current at listen(), not at the constructor — // DESIGN's stage 1 point 2). Kept out of ServerImpl's own fields so it stays off // the guest-visible Server shape; dispatch/connect/accept below read it back. const serverFrames = new WeakMap<ServerImpl, Context>();
class ServerImpl extends EventEmitter implements Server { #state: ServerState = { kind: "idle" }; #connections = new Set<ServerSocket>(); timeout = 0;
/** Server options (keepAlive, timeouts, IncomingMessage overrides) have nothing * to act on over the bridge and are accepted unread. */ constructor(options?: object | Listener, handler?: Listener) { super(); const listener = typeof options === "function" ? options as Listener : handler; if (listener) this.on("request", listener); // Every socket this server hands out (dispatch() and accept() both emit this) // is tracked here, so closeAllConnections() has something real to destroy. this.on("connection", (socket: ServerSocket) => { this.#connections.add(socket); if (this.timeout > 0) socket.setTimeout(this.timeout); socket.once("close", () => this.#connections.delete(socket)); }); }
get listening(): boolean { return this.#state.kind === "listening"; }
setTimeout(msecs: number, cb?: () => void): this { this.timeout = msecs; if (cb) this.once("timeout", cb); return this; }
closeAllConnections(): void { for (const socket of this.#connections) socket.destroy(); }
/** Nothing here outlives its one exchange (no keep-alive, no persistent * sockets), so there is never an idle connection to close. */ closeIdleConnections(): void {}
listen(port: number, ...rest: unknown[]): this { const cb = rest.find((a): a is Listener => typeof a === "function"); if (!Number.isInteger(port) || port < 0 || port > 65535) { throw nodeError("ERR_SOCKET_BAD_PORT", `options.port should be >= 0 and < 65536. Received ${String(port)}.`); } if (this.#state.kind === "binding" || this.#state.kind === "listening") { throw nodeError("ERR_SERVER_ALREADY_LISTEN", "Listen method has been called more than once without closing."); } if (cb) this.once("listening", cb); this.#state = { kind: "binding", port }; // Captured synchronously, in the guest's listen() call: every connection this // server accepts, and 'listening'/'error' below, run in it. const frame = capture(); serverFrames.set(this, frame); // The server holds the loop for as long as it listens; close() releases it. loop.hold(); call("listen", { port }).then( () => { if (this.#state.kind !== "binding") { // close() ran while binding: give the port back, keep the loop's books straight. void call("unlisten", { port }); loop.release(); return; } this.#state = { kind: "listening", port }; servers.set(port, this); loop.enqueueCompletion(() => runInFrame(frame, () => this.emit("listening"))); }, (e: Error & { code?: string }) => { this.#state = { kind: "idle" }; const err = nodeError(e.code ?? "EIO", `listen ${e.code ?? "EIO"}: ${e.message.replace(/^\w+: /, "")} :::${port}`, { errno: (e as { errno?: number }).errno, syscall: "listen", address: "::", port, }); loop.enqueueCompletion(() => runInFrame(frame, () => { try { this.emit("error", err); } finally { loop.release(); } })); }, ); return this; }
close(cb?: (err?: Error) => void): this { const state = this.#state; if (state.kind !== "listening") { // Node: close() on a server that is not running still emits 'close', and the // callback receives ERR_SERVER_NOT_RUNNING. if (state.kind === "binding") this.#state = { kind: "closed" }; if (cb) this.once("close", () => cb(nodeError("ERR_SERVER_NOT_RUNNING", "Server is not running."))); loop.enqueueCompletion(() => this.emit("close")); return this; } if (cb) this.once("close", cb); this.#state = { kind: "closed" }; servers.delete(state.port); const frame = serverFrames.get(this) ?? capture(); // Emit before release: release() can run onIdle synchronously, and the exit // must not overtake the 'close' listeners' output. const closed = (): void => loop.enqueueCompletion(() => runInFrame(frame, () => { try { this.emit("close"); } finally { loop.release(); } })); call("unlisten", { port: state.port }).then(closed, closed); return this; }
address(): { address: string; family: string; port: number } | null { return this.#state.kind === "listening" ? { address: "::", family: "IPv6", port: this.#state.port } : null; } }
function dispatch({ id, port, req }: { id: number; port: number; req: HttpRequestRecord }): void { const server = servers.get(port); if (!server) { // Raced a close(): the kernel routed before our unlisten landed. deps.respond(id, { status: 502, headers: [["content-type", "text/plain; charset=utf-8"]], body: enc.encode(`ERR_NIB_NO_LISTENER: nothing is listening on port ${port}\n`), }); return; } const frame = serverFrames.get(server) ?? capture(); loop.enqueueCompletion(() => runInFrame(frame, () => { let sent = false; const send = (record: HttpResponseRecord): void => { if (sent) return; sent = true; deps.respond(id, record); }; const socket = new VirtualSocket(server, port, (error) => { if (sent) { // Node dumps a body nobody read once the response is out. incoming._dump(); } else { // Destroyed before end(): the client sees the connection fail, here a 502. send({ status: 502, headers: [["content-type", "text/plain; charset=utf-8"]], body: enc.encode(`ERR_NIB_SOCKET_DESTROYED: the server closed the connection without responding${error ? ` (${error.message})` : ""}\n`), }); incoming.aborted = true; incoming.destroy(); } res._socketClosed(); }); const incoming = new IncomingMessage(withTransportHeaders(req, port), socket); const res = new ServerResponse(incoming, send); server.emit("connection", socket); server.emit("request", incoming, res); })); }
/** A raw connection: buffer bytes until the request head is complete, then the port * belongs to a PortSocket and the request goes to 'upgrade' (or 'request'). */ function connect({ id, port, socket: wire }: { id: number; port: number; socket: MessagePort }): void { let buffered: Uint8Array = new Uint8Array(0); loop.hold(); wire.onmessage = (ev: MessageEvent<ConnectionData>) => { const data = ev.data; if (data === null) { // The page gave up before its request was complete. wire.close(); loop.release(); deps.closed(id); return; } buffered = concatBytes(buffered, data); const end = headEnd(buffered); if (end === -1 && buffered.length <= MAX_HEADER_BYTES) return; const server = servers.get(port) ?? null; const frame = server ? serverFrames.get(server) ?? capture() : capture(); const socket = new PortSocket(server, port, wire, loop, () => deps.closed(id), frame); loop.release(); const head = buffered; loop.enqueueCompletion(() => runInFrame(frame, () => accept(server, socket, head, end))); }; }
function accept(server: ServerImpl | null, socket: PortSocket, bytes: Uint8Array, end: number): void { if (!server) { socket.end(rawHttpResponse(502, "Bad Gateway", `ERR_NIB_NO_LISTENER: nothing is listening on port ${socket.localPort}\n`)); return; } server.emit("connection", socket); const record = end === -1 ? null : parseRequestHead(bytes.subarray(0, end)); if (!record) { const err = nodeError(end === -1 ? "HPE_HEADER_OVERFLOW" : "HPE_INVALID_HEADER_TOKEN", end === -1 ? "Header overflow" : "Parse Error: Invalid header token"); if (server.listenerCount("clientError") > 0) server.emit("clientError", err, socket); else socket.end(end === -1 ? CLIENT_ERROR_431 : CLIENT_ERROR_400); return; } const req = new IncomingMessage(record, socket); const head = bytes.subarray(end + 4); if (wantsUpgrade(req) && server.listenerCount("upgrade") > 0) { req.upgrade = true; server.emit("upgrade", req, socket, new Buffer(head.buffer as ArrayBuffer, head.byteOffset, head.byteLength)); return; } // Node serves an upgrade request nobody upgrades as a plain request. The connection // carries this one exchange and closes after it. const res = new ServerResponse(req, (out) => socket.end(serializeResponse(out, res.statusMessage))); socket.once("close", () => res._socketClosed()); server.emit("request", req, res); }
/** Browsers never expose Host or Content-Length to a Service Worker, but the wire * request carried them: Host for `localhost:<port>`, and the Fetch standard's * Content-Length (the body's length, or 0 for a bodiless POST or PUT). Body * parsers (body-parser's type-is) only read a body whose length is declared. */ function withTransportHeaders(req: HttpRequestRecord, port: number): HttpRequestRecord { const has = (header: string): boolean => req.headers.some(([name]) => name === header); const headers = [...req.headers]; if (!has("host")) headers.unshift(["host", `localhost:${port}`]); const length = req.body?.length ?? 0; if (!has("content-length") && !has("transfer-encoding") && (length > 0 || req.method === "POST" || req.method === "PUT")) { headers.push(["content-length", String(length)]); } return { ...req, headers }; }
return { module: { createServer: (options?: object | Listener, handler?: Listener) => new ServerImpl(options, handler), Server: ServerImpl, IncomingMessage, ServerResponse, OutgoingMessage, ClientRequest, STATUS_CODES, METHODS, request: refused("http", "request", CLIENT_WHY), get: refused("http", "get", CLIENT_WHY), Agent, globalAgent, validateHeaderName, validateHeaderValue, // A getter, like Node's own: assigning to it (some code does, defensively) // silently does nothing instead of actually resizing anything. get maxHeaderSize() { return maxHeaderSize; }, setMaxIdleHTTPParsers, setGlobalProxyFromEnv, // Re-exports, as Node 22+ does: http.WebSocket *is* the global WebSocket. WebSocket: globalThis.WebSocket, CloseEvent: globalThis.CloseEvent, MessageEvent: globalThis.MessageEvent, }, dispatch, connect, };}