UNPKG

@mjackson/node-fetch-server

Version:
8 lines (7 loc) 11.3 kB
{ "version": 3, "sources": ["../src/node-fetch-server.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 while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n yield value;\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 '@mjackson/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 = new AbortController();\n res.on('close', () => {\n controller.abort();\n });\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 res.writeHead(response.status, headers);\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 res.write(chunk);\n }\n }\n\n res.end();\n}\n"], "mappings": "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", "names": ["error", "controller"] }