/** * node:http ↔ WHATWG fetch bridge for the /api transport (host side of the * web carrier; the fetch-shaped handler itself is transport-agnostic). */ import type { IncomingMessage, ServerResponse } from 'node:http' /** * Bridge one node:http request to the fetch-shaped handler (client close * aborts; SSE bodies stream out chunk by chunk). * @param req - incoming node:http request (fully read before dispatch). * @param res - node:http response the bridge writes and owns to completion. * @param apiHandler - fetch-shaped API carrier the request is dispatched to. */ export async function bridge(req: IncomingMessage, res: ServerResponse, apiHandler: { fetch: typeof fetch }): Promise { const abort = new AbortController() // Client-disconnect detection MUST hang off the response, not the request: // since Node 16, IncomingMessage 'close' fires as soon as the request body is // fully consumed (immediately for a bodyless GET), which would abort every SSE // stream right after open. ServerResponse 'close' fires on connection teardown; // writableEnded distinguishes a normal end() from the client going away. res.on('close', () => { if (!res.writableEnded) abort.abort() }) const chunks: Buffer[] = [] for await (const chunk of req) chunks.push(chunk as Buffer) /* v8 ignore next 3 -- `??` arms: node:http always sets url/method on server requests; the fields are only optional on the client-side IncomingMessage type */ const request = new Request(new URL(req.url ?? '/', 'http://dsh.internal'), { method: req.method ?? 'GET', headers: Object.fromEntries(Object.entries(req.headers).filter(([, v]) => typeof v === 'string') as [string, string][]), ...chunks.length > 0 ? { body: Buffer.concat(chunks) } : {}, signal: abort.signal, }) const response = await apiHandler.fetch(request) res.writeHead(response.status, Object.fromEntries(response.headers.entries())) if (response.body === null) { res.end() return } for await (const chunk of response.body) { // Backpressure: a false return means the socket buffer is full — wait for drain // instead of buffering unboundedly (slow/suspended SSE consumers). 'close' also // resolves so a mid-wait disconnect can't park this loop forever; the close // handler above aborts the handler stream, which then ends the iteration. if (!res.write(chunk)) { await new Promise((resolve) => { const done = (): void => { res.off('drain', done) res.off('close', done) resolve() } res.once('drain', done) res.once('close', done) }) } } res.end() }