@remix-run/node-fetch-server
Version:
Build servers for Node.js using the web fetch API
8 lines (7 loc) • 12.5 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/index.ts", "../src/lib/read-stream.ts", "../src/lib/request-listener.ts"],
"sourcesContent": ["export { type ClientAddress, type ErrorHandler, type FetchHandler } from './lib/fetch-handler.ts'\nexport {\n type RequestListenerOptions,\n createRequestListener,\n type RequestOptions,\n createRequest,\n createHeaders,\n sendResponse,\n} from './lib/request-listener.ts'\n", "export async function* readStream(stream: ReadableStream<Uint8Array>): AsyncIterable<Uint8Array> {\n let reader = stream.getReader()\n\n try {\n while (true) {\n let result = await reader.read()\n if (result.done) break\n yield result.value\n }\n } finally {\n reader.releaseLock()\n }\n}\n", "import type * as http from 'node:http'\nimport type * as http2 from 'node:http2'\n\nimport type { ClientAddress, ErrorHandler, FetchHandler } from './fetch-handler.ts'\nimport { readStream } from './read-stream.ts'\n\nexport interface RequestListenerOptions {\n /**\n * Overrides the host portion of the incoming request URL. By default the request URL host is\n * derived from the HTTP `Host` header.\n *\n * For example, if you have a `$HOST` environment variable that contains the hostname of your\n * server, you can use it to set the host of all incoming request URLs like so:\n *\n * ```ts\n * createRequestListener(handler, { host: process.env.HOST })\n * ```\n */\n host?: string\n /**\n * An error handler that determines the response when the request handler throws an error. By\n * default a 500 Internal Server Error response will be sent.\n */\n onError?: ErrorHandler\n /**\n * Overrides the protocol of the incoming request URL. By default the request URL protocol is\n * derived from the connection protocol. So e.g. when serving over HTTPS (using\n * `https.createServer()`), the request URL will begin with `https:`.\n */\n protocol?: string\n}\n\n/**\n * Wraps a fetch handler in a Node.js request listener that can be used with:\n *\n * - [`http.createServer()`](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener)\n * - [`https.createServer()`](https://nodejs.org/api/https.html#httpscreateserveroptions-requestlistener)\n * - [`http2.createServer()`](https://nodejs.org/api/http2.html#http2createserveroptions-onrequesthandler)\n * - [`http2.createSecureServer()`](https://nodejs.org/api/http2.html#http2createsecureserveroptions-onrequesthandler)\n *\n * Example:\n *\n * ```ts\n * import * as http from 'node:http';\n * import { createRequestListener } from '@remix-run/node-fetch-server';\n *\n * async function handler(request) {\n * return new Response('Hello, world!');\n * }\n *\n * let server = http.createServer(\n * createRequestListener(handler)\n * );\n *\n * server.listen(3000);\n * ```\n *\n * @param handler The fetch handler to use for processing incoming requests.\n * @param options Request listener options.\n * @returns A Node.js request listener function.\n */\nexport function createRequestListener(\n handler: FetchHandler,\n options?: RequestListenerOptions,\n): http.RequestListener {\n let onError = options?.onError ?? defaultErrorHandler\n\n return async (req, res) => {\n let request = createRequest(req, res, options)\n let client = {\n address: req.socket.remoteAddress!,\n family: req.socket.remoteFamily! as ClientAddress['family'],\n port: req.socket.remotePort!,\n }\n\n let response: Response\n try {\n response = await handler(request, client)\n } catch (error) {\n try {\n response = (await onError(error)) ?? internalServerError()\n } catch (error) {\n console.error(`There was an error in the error handler: ${error}`)\n response = internalServerError()\n }\n }\n\n await sendResponse(res, response)\n }\n}\n\nfunction defaultErrorHandler(error: unknown): Response {\n console.error(error)\n return internalServerError()\n}\n\nfunction internalServerError(): Response {\n return new Response(\n // \"Internal Server Error\"\n new Uint8Array([\n 73, 110, 116, 101, 114, 110, 97, 108, 32, 83, 101, 114, 118, 101, 114, 32, 69, 114, 114, 111,\n 114,\n ]),\n {\n status: 500,\n headers: {\n 'Content-Type': 'text/plain',\n },\n },\n )\n}\n\nexport type RequestOptions = Omit<RequestListenerOptions, 'onError'>\n\n/**\n * Creates a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object from\n *\n * - a [`http.IncomingMessage`](https://nodejs.org/api/http.html#class-httpincomingmessage)/[`http.ServerResponse`](https://nodejs.org/api/http.html#class-httpserverresponse) pair\n * - a [`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#class-http2http2serverrequest)/[`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#class-http2http2serverresponse) pair\n *\n * @param req The incoming request object.\n * @param res The server response object.\n * @param options\n * @returns A request object.\n */\nexport function createRequest(\n req: http.IncomingMessage | http2.Http2ServerRequest,\n res: http.ServerResponse | http2.Http2ServerResponse,\n options?: RequestOptions,\n): Request {\n let controller: AbortController | null = new AbortController()\n\n // Abort once we can no longer write a response if we have\n // not yet sent a response (i.e., `close` without `finish`)\n // `finish` -> done rendering the response\n // `close` -> response can no longer be written to\n res.on('close', () => controller?.abort())\n res.on('finish', () => (controller = null))\n\n let method = req.method ?? 'GET'\n let headers = createHeaders(req)\n\n let protocol =\n options?.protocol ?? ('encrypted' in req.socket && req.socket.encrypted ? 'https:' : 'http:')\n let host = options?.host ?? headers.get('Host') ?? 'localhost'\n let url = new URL(req.url!, `${protocol}//${host}`)\n\n let init: RequestInit = { method, headers, signal: controller.signal }\n\n if (method !== 'GET' && method !== 'HEAD') {\n init.body = new ReadableStream({\n start(controller) {\n req.on('data', (chunk) => {\n controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength))\n })\n req.on('end', () => {\n controller.close()\n })\n },\n })\n\n // init.duplex = 'half' must be set when body is a ReadableStream, and Node follows the spec.\n // However, this property is not defined in the TypeScript types for RequestInit, so we have\n // to cast it here in order to set it without a type error.\n // See https://fetch.spec.whatwg.org/#dom-requestinit-duplex\n ;(init as { duplex: 'half' }).duplex = 'half'\n }\n\n return new Request(url, init)\n}\n\n/**\n * Creates a [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object from the headers in a Node.js\n * [`http.IncomingMessage`](https://nodejs.org/api/http.html#class-httpincomingmessage)/[`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#class-http2http2serverrequest).\n *\n * @param req The incoming request object.\n * @returns A headers object.\n */\nexport function createHeaders(req: http.IncomingMessage | http2.Http2ServerRequest): Headers {\n let headers = new Headers()\n\n let rawHeaders = req.rawHeaders\n for (let i = 0; i < rawHeaders.length; i += 2) {\n if (rawHeaders[i].startsWith(':')) continue\n headers.append(rawHeaders[i], rawHeaders[i + 1])\n }\n\n return headers\n}\n\n/**\n * Sends a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) to the client using a Node.js\n * [`http.ServerResponse`](https://nodejs.org/api/http.html#class-httpserverresponse)/[`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#class-http2http2serverresponse)\n * object.\n *\n * @param res The server response object.\n * @param response The response to send.\n */\nexport async function sendResponse(\n res: http.ServerResponse | http2.Http2ServerResponse,\n response: Response,\n): Promise<void> {\n // Iterate over response.headers so we are sure to send multiple Set-Cookie headers correctly.\n // These would incorrectly be merged into a single header if we tried to use\n // `Object.fromEntries(response.headers.entries())`.\n let headers: Record<string, string | string[]> = {}\n for (let [key, value] of response.headers) {\n if (key in headers) {\n if (Array.isArray(headers[key])) {\n headers[key].push(value)\n } else {\n headers[key] = [headers[key] as string, value]\n }\n } else {\n headers[key] = value\n }\n }\n\n if (res.req.httpVersionMajor === 1) {\n res.writeHead(response.status, response.statusText, headers)\n } else {\n // HTTP/2 doesn't support status messages\n // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.4\n //\n // HTTP2 `res.writeHead()` will safely ignore the statusText parameter, but\n // it will emit a warning which we want to avoid.\n // https://nodejs.org/docs/latest-v22.x/api/http2.html#responsewriteheadstatuscode-statusmessage-headers\n res.writeHead(response.status, headers)\n }\n\n if (response.body != null && res.req.method !== 'HEAD') {\n for await (let chunk of readStream(response.body)) {\n // @ts-expect-error - Node typings for http2 require a 2nd parameter to write but it's optional\n if (res.write(chunk) === false) {\n await new Promise<void>((resolve) => {\n res.once('drain', resolve)\n })\n }\n }\n }\n\n res.end()\n}\n"],
"mappings": "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",
"names": ["error", "controller"]
}