import { createServer as createHttpServer } from 'node:http' import type { AddressInfo } from 'node:net' import { Readable } from 'node:stream' import type { ReadableStream as NodeReadableStream } from 'node:stream/web' import type { IncomingMessage, ServerResponse } from 'node:http' import type { FetchCallback, HttpBindings, ServeOptions, ServerType } from './types.js' type RequestInitWithDuplex = RequestInit & { duplex?: 'half' } const hasRequestBody = (incoming: IncomingMessage): boolean => incoming.method !== 'GET' && incoming.method !== 'HEAD' const headersFromIncoming = (incoming: IncomingMessage): Headers => { const headers = new Headers() for (let i = 0; i < incoming.rawHeaders.length; i += 2) { headers.append(incoming.rawHeaders[i], incoming.rawHeaders[i + 1]) } return headers } const requestUrl = (incoming: IncomingMessage, hostname?: string): string => { const incomingUrl = incoming.url || '/' if (incomingUrl.startsWith('http://') || incomingUrl.startsWith('https://')) { return incomingUrl } const host = incoming.headers.host || hostname if (!host) { throw new Error('Missing host header') } const protocol = incoming.socket && 'encrypted' in incoming.socket ? 'https' : 'http' return new URL(incomingUrl, `${protocol}://${host}`).href } const toRequest = ( incoming: IncomingMessage, outgoing: ServerResponse, hostname?: string ): Request => { const controller = new AbortController() outgoing.on('close', () => { if (!outgoing.writableFinished) { controller.abort() } }) const init: RequestInitWithDuplex = { method: incoming.method, headers: headersFromIncoming(incoming), signal: controller.signal, } if (hasRequestBody(incoming)) { init.body = Readable.toWeb(incoming) as ReadableStream init.duplex = 'half' } return new Request(requestUrl(incoming, hostname), init) } const outgoingHeaders = (headers: Headers): Record => { const output: Record = {} const getSetCookie = (headers as Headers & { getSetCookie?: () => string[] }).getSetCookie const setCookie = getSetCookie?.call(headers) for (const [key, value] of headers) { if (key !== 'set-cookie') { output[key] = value } } if (setCookie && setCookie.length > 0) { output['set-cookie'] = setCookie } return output } const writeResponse = async (response: Response, outgoing: ServerResponse): Promise => { outgoing.writeHead(response.status, outgoingHeaders(response.headers)) if (!response.body) { outgoing.end() return } await new Promise((resolve, reject) => { const body = Readable.fromWeb(response.body as unknown as NodeReadableStream) body.on('error', reject) outgoing.on('error', reject) outgoing.on('finish', resolve) body.pipe(outgoing) }) } const errorResponse = (status: number): Response => new Response(null, { status, }) export const getRequestListener = ( fetchCallback: FetchCallback, options: { hostname?: string } = {} ) => { return async (incoming: IncomingMessage, outgoing: ServerResponse): Promise => { let request: Request try { request = toRequest(incoming, outgoing, options.hostname) } catch { await writeResponse(errorResponse(400), outgoing) return } let response: Response try { response = await fetchCallback(request, { incoming, outgoing } satisfies HttpBindings) } catch { response = errorResponse(500) } try { await writeResponse(response, outgoing) } catch { if (!outgoing.headersSent) { outgoing.writeHead(500) } outgoing.end() } } } export const createAdaptorServer = (options: ServeOptions): ServerType => { const createServer = options.createServer || createHttpServer return createServer(options.serverOptions || {}, getRequestListener(options.fetch, options)) } export const serve = ( options: ServeOptions, listeningListener?: (info: AddressInfo) => void ): ServerType => { const server = createAdaptorServer(options) server.listen(options.port ?? 3000, options.hostname, () => { listeningListener?.(server.address() as AddressInfo) }) return server }