Socket
Book a DemoSign in
Socket

@mjackson/node-fetch-server

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mjackson/node-fetch-server - npm Package Compare versions

Comparing version
0.6.1
to
0.7.0
+40
dist/lib/fetch-handler.d.ts
export interface ClientAddress {
/**
* The IP address of the client that sent the request.
*
* [Node.js Reference](https://nodejs.org/api/net.html#socketremoteaddress)
*/
address: string;
/**
* The family of the client IP address.
*
* [Node.js Reference](https://nodejs.org/api/net.html#socketremotefamily)
*/
family: 'IPv4' | 'IPv6';
/**
* The remote port of the client that sent the request.
*
* [Node.js Reference](https://nodejs.org/api/net.html#socketremoteport)
*/
port: number;
}
/**
* A function that handles an error that occurred during request handling. May return a response to
* send to the client, or `undefined` to allow the server to send a default error response.
*
* [MDN `Response` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response)
*/
export interface ErrorHandler {
(error: unknown): void | Response | Promise<void | Response>;
}
/**
* A function that handles an incoming request and returns a response.
*
* [MDN `Request` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Request)
*
* [MDN `Response` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response)
*/
export interface FetchHandler {
(request: Request, client: ClientAddress): Response | Promise<Response>;
}
//# sourceMappingURL=fetch-handler.d.ts.map
{"version":3,"file":"fetch-handler.d.ts","sourceRoot":"","sources":["../../src/lib/fetch-handler.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,GAAG,QAAQ,GAAG,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC,CAAC;CAC9D;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACzE"}
export declare function readStream(stream: ReadableStream<Uint8Array>): AsyncIterable<Uint8Array>;
//# sourceMappingURL=read-stream.d.ts.map
{"version":3,"file":"read-stream.d.ts","sourceRoot":"","sources":["../../src/lib/read-stream.ts"],"names":[],"mappings":"AAAA,wBAAuB,UAAU,CAAC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,CAQ/F"}
import type * as http from 'node:http';
import type * as http2 from 'node:http2';
import type { ErrorHandler, FetchHandler } from './fetch-handler.ts';
export interface RequestListenerOptions {
/**
* Overrides the host portion of the incoming request URL. By default the request URL host is
* derived from the HTTP `Host` header.
*
* For example, if you have a `$HOST` environment variable that contains the hostname of your
* server, you can use it to set the host of all incoming request URLs like so:
*
* ```ts
* createRequestListener(handler, { host: process.env.HOST })
* ```
*/
host?: string;
/**
* An error handler that determines the response when the request handler throws an error. By
* default a 500 Internal Server Error response will be sent.
*/
onError?: ErrorHandler;
/**
* Overrides the protocol of the incoming request URL. By default the request URL protocol is
* derived from the connection protocol. So e.g. when serving over HTTPS (using
* `https.createServer()`), the request URL will begin with `https:`.
*/
protocol?: string;
}
/**
* Wraps a fetch handler in a Node.js request listener that can be used with:
*
* - [`http.createServer()`](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener)
* - [`https.createServer()`](https://nodejs.org/api/https.html#httpscreateserveroptions-requestlistener)
* - [`http2.createServer()`](https://nodejs.org/api/http2.html#http2createserveroptions-onrequesthandler)
* - [`http2.createSecureServer()`](https://nodejs.org/api/http2.html#http2createsecureserveroptions-onrequesthandler)
*
* Example:
*
* ```ts
* import * as http from 'node:http';
* import { createRequestListener } from '@mjackson/node-fetch-server';
*
* async function handler(request) {
* return new Response('Hello, world!');
* }
*
* let server = http.createServer(
* createRequestListener(handler)
* );
*
* server.listen(3000);
* ```
*
* @param handler The fetch handler to use for processing incoming requests.
* @param options Request listener options.
* @returns A Node.js request listener function.
*/
export declare function createRequestListener(handler: FetchHandler, options?: RequestListenerOptions): http.RequestListener;
export type RequestOptions = Omit<RequestListenerOptions, 'onError'>;
/**
* Creates a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object from
*
* - a [`http.IncomingMessage`](https://nodejs.org/api/http.html#class-httpincomingmessage)/[`http.ServerResponse`](https://nodejs.org/api/http.html#class-httpserverresponse) pair
* - a [`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#class-http2http2serverrequest)/[`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#class-http2http2serverresponse) pair
*
* @param req The incoming request object.
* @param res The server response object.
* @param options
* @returns A request object.
*/
export declare function createRequest(req: http.IncomingMessage | http2.Http2ServerRequest, res: http.ServerResponse | http2.Http2ServerResponse, options?: RequestOptions): Request;
/**
* Creates a [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object from the headers in a Node.js
* [`http.IncomingMessage`](https://nodejs.org/api/http.html#class-httpincomingmessage)/[`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#class-http2http2serverrequest).
*
* @param req The incoming request object.
* @returns A headers object.
*/
export declare function createHeaders(req: http.IncomingMessage | http2.Http2ServerRequest): Headers;
/**
* Sends a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) to the client using a Node.js
* [`http.ServerResponse`](https://nodejs.org/api/http.html#class-httpserverresponse)/[`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#class-http2http2serverresponse)
* object.
*
* @param res The server response object.
* @param response The response to send.
*/
export declare function sendResponse(res: http.ServerResponse | http2.Http2ServerResponse, response: Response): Promise<void>;
//# sourceMappingURL=request-listener.d.ts.map
{"version":3,"file":"request-listener.d.ts","sourceRoot":"","sources":["../../src/lib/request-listener.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,KAAK,KAAK,MAAM,YAAY,CAAC;AAEzC,OAAO,KAAK,EAAiB,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGpF,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;;;OAUG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,YAAY,EACrB,OAAO,CAAC,EAAE,sBAAsB,GAC/B,IAAI,CAAC,eAAe,CAyBtB;AAuBD,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,sBAAsB,EAAE,SAAS,CAAC,CAAC;AAErE;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC,kBAAkB,EACpD,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,mBAAmB,EACpD,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAoCT;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC,kBAAkB,GAAG,OAAO,CAU3F;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,KAAK,CAAC,mBAAmB,EACpD,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,IAAI,CAAC,CA2Bf"}
{
"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"]
}
{"version":3,"file":"node-fetch-server.d.ts","sourceRoot":"","sources":["../src/node-fetch-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAClG,OAAO,EACL,KAAK,sBAAsB,EAC3B,qBAAqB,EACrB,KAAK,cAAc,EACnB,aAAa,EACb,aAAa,EACb,YAAY,GACb,MAAM,2BAA2B,CAAC"}
{
"version": 3,
"sources": ["../src/lib/read-stream.ts", "../src/lib/request-listener.ts"],
"sourcesContent": ["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"]
}
export interface ClientAddress {
/**
* The IP address of the client that sent the request.
*
* [Node.js Reference](https://nodejs.org/api/net.html#socketremoteaddress)
*/
address: string;
/**
* The family of the client IP address.
*
* [Node.js Reference](https://nodejs.org/api/net.html#socketremotefamily)
*/
family: 'IPv4' | 'IPv6';
/**
* The remote port of the client that sent the request.
*
* [Node.js Reference](https://nodejs.org/api/net.html#socketremoteport)
*/
port: number;
}
/**
* A function that handles an error that occurred during request handling. May return a response to
* send to the client, or `undefined` to allow the server to send a default error response.
*
* [MDN `Response` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response)
*/
export interface ErrorHandler {
(error: unknown): void | Response | Promise<void | Response>;
}
/**
* A function that handles an incoming request and returns a response.
*
* [MDN `Request` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Request)
*
* [MDN `Response` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response)
*/
export interface FetchHandler {
(request: Request, client: ClientAddress): Response | Promise<Response>;
}
export async function* readStream(stream: ReadableStream<Uint8Array>): AsyncIterable<Uint8Array> {
let reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield value;
}
}
import * as assert from 'node:assert/strict';
import { describe, it, mock } from 'node:test';
import * as http from 'node:http';
import * as stream from 'node:stream';
import { type FetchHandler } from './fetch-handler.ts';
import { createRequestListener } from './request-listener.ts';
describe('createRequestListener', () => {
it('returns a request listener', async () => {
await new Promise<void>((resolve) => {
let handler: FetchHandler = async () => {
return new Response('Hello, world!');
};
let listener = createRequestListener(handler);
assert.ok(listener);
let req = createMockRequest();
let res = createMockResponse({ req });
let chunks: Uint8Array[] = [];
mock.method(res, 'write', (chunk: Uint8Array) => {
chunks.push(chunk);
});
mock.method(res, 'end', () => {
let body = Buffer.concat(chunks).toString();
assert.equal(body, 'Hello, world!');
resolve();
});
listener(req, res);
});
});
it('calls onError when an error is thrown in the request handler', async () => {
await new Promise<void>((resolve) => {
let handler: FetchHandler = async () => {
throw new Error('boom!');
};
let errorHandler = mock.fn();
let listener = createRequestListener(handler, { onError: errorHandler });
assert.ok(listener);
let req = createMockRequest();
let res = createMockResponse({ req });
mock.method(res, 'end', () => {
assert.equal(errorHandler.mock.calls.length, 1);
resolve();
});
listener(req, res);
});
});
it('returns a 500 "Internal Server Error" response when an error is thrown in the request handler', async () => {
await new Promise<void>((resolve) => {
let handler: FetchHandler = async () => {
throw new Error('boom!');
};
let errorHandler = async () => {
// ignore
};
let listener = createRequestListener(handler, { onError: errorHandler });
assert.ok(listener);
let req = createMockRequest();
let res = createMockResponse({ req });
let status: number | undefined;
mock.method(res, 'writeHead', (statusCode: number) => {
status = statusCode;
});
let chunks: Uint8Array[] = [];
mock.method(res, 'write', (chunk: Uint8Array) => {
chunks.push(chunk);
});
mock.method(res, 'end', () => {
assert.equal(status, 500);
let body = Buffer.concat(chunks).toString();
assert.equal(body, 'Internal Server Error');
resolve();
});
listener(req, res);
});
});
it('uses the `Host` header to construct the URL by default', async () => {
await new Promise<void>((resolve) => {
let handler: FetchHandler = async (request) => {
assert.equal(request.url, 'http://example.com/');
return new Response('Hello, world!');
};
let listener = createRequestListener(handler);
assert.ok(listener);
let req = createMockRequest({ headers: { host: 'example.com' } });
let res = createMockResponse({ req });
listener(req, res);
resolve();
});
});
it('uses the `host` option to override the `Host` header', async () => {
await new Promise<void>((resolve) => {
let handler: FetchHandler = async (request) => {
assert.equal(request.url, 'http://remix.run/');
return new Response('Hello, world!');
};
let listener = createRequestListener(handler, { host: 'remix.run' });
assert.ok(listener);
let req = createMockRequest({ headers: { host: 'example.com' } });
let res = createMockResponse({ req });
listener(req, res);
resolve();
});
});
it('uses the `protocol` option to construct the URL', async () => {
await new Promise<void>((resolve) => {
let handler: FetchHandler = async (request) => {
assert.equal(request.url, 'https://example.com/');
return new Response('Hello, world!');
};
let listener = createRequestListener(handler, { protocol: 'https:' });
assert.ok(listener);
let req = createMockRequest({ headers: { host: 'example.com' } });
let res = createMockResponse({ req });
listener(req, res);
resolve();
});
});
it('sets multiple Set-Cookie headers', async () => {
await new Promise<void>((resolve) => {
let handler: FetchHandler = async () => {
let headers = new Headers();
headers.set('Content-Type', 'text/plain');
headers.append('Set-Cookie', 'a=1');
headers.append('Set-Cookie', 'b=2');
return new Response('Hello, world!', { headers });
};
let listener = createRequestListener(handler);
assert.ok(listener);
let req = createMockRequest();
let res = createMockResponse({ req });
let headers: string[];
mock.method(res, 'writeHead', (_status: number, headersArray: string[]) => {
headers = headersArray;
});
mock.method(res, 'end', () => {
assert.deepEqual(headers, {
'content-type': 'text/plain',
'set-cookie': ['a=1', 'b=2'],
});
resolve();
});
listener(req, res);
});
});
it('truncates the response body when the request method is HEAD', async () => {
await new Promise<void>((resolve) => {
let handler: FetchHandler = async () => {
return new Response('Hello, world!');
};
let listener = createRequestListener(handler);
assert.ok(listener);
let req = createMockRequest({ method: 'HEAD' });
let res = createMockResponse({ req });
let chunks: Uint8Array[] = [];
mock.method(res, 'write', (chunk: Uint8Array) => {
chunks.push(chunk);
});
mock.method(res, 'end', () => {
assert.equal(chunks.length, 0);
resolve();
});
listener(req, res);
});
});
});
function createMockRequest({
url = '/',
method = 'GET',
headers = {},
socket = {},
body,
}: {
method?: string;
url?: string;
headers?: Record<string, string>;
socket?: {
encrypted?: boolean;
remoteAddress?: string;
};
body?: string | Buffer;
} = {}): http.IncomingMessage {
let rawHeaders = Object.entries(headers).flatMap(([key, value]) => [key, value]);
return Object.assign(
new stream.Readable({
read() {
if (body != null) this.push(Buffer.from(body));
this.push(null);
},
}),
{
url,
method,
rawHeaders,
socket,
},
) as http.IncomingMessage;
}
function createMockResponse({
req = createMockRequest(),
}: {
req: http.IncomingMessage;
}): http.ServerResponse {
return Object.assign(new stream.Writable(), {
req,
writeHead() {},
write() {},
end() {},
}) as unknown as http.ServerResponse;
}
import type * as http from 'node:http';
import type * as http2 from 'node:http2';
import type { ClientAddress, ErrorHandler, FetchHandler } from './fetch-handler.ts';
import { readStream } from './read-stream.ts';
export interface RequestListenerOptions {
/**
* Overrides the host portion of the incoming request URL. By default the request URL host is
* derived from the HTTP `Host` header.
*
* For example, if you have a `$HOST` environment variable that contains the hostname of your
* server, you can use it to set the host of all incoming request URLs like so:
*
* ```ts
* createRequestListener(handler, { host: process.env.HOST })
* ```
*/
host?: string;
/**
* An error handler that determines the response when the request handler throws an error. By
* default a 500 Internal Server Error response will be sent.
*/
onError?: ErrorHandler;
/**
* Overrides the protocol of the incoming request URL. By default the request URL protocol is
* derived from the connection protocol. So e.g. when serving over HTTPS (using
* `https.createServer()`), the request URL will begin with `https:`.
*/
protocol?: string;
}
/**
* Wraps a fetch handler in a Node.js request listener that can be used with:
*
* - [`http.createServer()`](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener)
* - [`https.createServer()`](https://nodejs.org/api/https.html#httpscreateserveroptions-requestlistener)
* - [`http2.createServer()`](https://nodejs.org/api/http2.html#http2createserveroptions-onrequesthandler)
* - [`http2.createSecureServer()`](https://nodejs.org/api/http2.html#http2createsecureserveroptions-onrequesthandler)
*
* Example:
*
* ```ts
* import * as http from 'node:http';
* import { createRequestListener } from '@mjackson/node-fetch-server';
*
* async function handler(request) {
* return new Response('Hello, world!');
* }
*
* let server = http.createServer(
* createRequestListener(handler)
* );
*
* server.listen(3000);
* ```
*
* @param handler The fetch handler to use for processing incoming requests.
* @param options Request listener options.
* @returns A Node.js request listener function.
*/
export function createRequestListener(
handler: FetchHandler,
options?: RequestListenerOptions,
): http.RequestListener {
let onError = options?.onError ?? defaultErrorHandler;
return async (req, res) => {
let request = createRequest(req, res, options);
let client = {
address: req.socket.remoteAddress!,
family: req.socket.remoteFamily! as ClientAddress['family'],
port: req.socket.remotePort!,
};
let response: Response;
try {
response = await handler(request, client);
} catch (error) {
try {
response = (await onError(error)) ?? internalServerError();
} catch (error) {
console.error(`There was an error in the error handler: ${error}`);
response = internalServerError();
}
}
await sendResponse(res, response);
};
}
function defaultErrorHandler(error: unknown): Response {
console.error(error);
return internalServerError();
}
function internalServerError(): Response {
return new Response(
// "Internal Server Error"
new Uint8Array([
73, 110, 116, 101, 114, 110, 97, 108, 32, 83, 101, 114, 118, 101, 114, 32, 69, 114, 114, 111,
114,
]),
{
status: 500,
headers: {
'Content-Type': 'text/plain',
},
},
);
}
export type RequestOptions = Omit<RequestListenerOptions, 'onError'>;
/**
* Creates a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object from
*
* - a [`http.IncomingMessage`](https://nodejs.org/api/http.html#class-httpincomingmessage)/[`http.ServerResponse`](https://nodejs.org/api/http.html#class-httpserverresponse) pair
* - a [`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#class-http2http2serverrequest)/[`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#class-http2http2serverresponse) pair
*
* @param req The incoming request object.
* @param res The server response object.
* @param options
* @returns A request object.
*/
export function createRequest(
req: http.IncomingMessage | http2.Http2ServerRequest,
res: http.ServerResponse | http2.Http2ServerResponse,
options?: RequestOptions,
): Request {
let controller = new AbortController();
res.on('close', () => {
controller.abort();
});
let method = req.method ?? 'GET';
let headers = createHeaders(req);
let protocol =
options?.protocol ?? ('encrypted' in req.socket && req.socket.encrypted ? 'https:' : 'http:');
let host = options?.host ?? headers.get('Host') ?? 'localhost';
let url = new URL(req.url!, `${protocol}//${host}`);
let init: RequestInit = { method, headers, signal: controller.signal };
if (method !== 'GET' && method !== 'HEAD') {
init.body = new ReadableStream({
start(controller) {
req.on('data', (chunk) => {
controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength));
});
req.on('end', () => {
controller.close();
});
},
});
// init.duplex = 'half' must be set when body is a ReadableStream, and Node follows the spec.
// However, this property is not defined in the TypeScript types for RequestInit, so we have
// to cast it here in order to set it without a type error.
// See https://fetch.spec.whatwg.org/#dom-requestinit-duplex
(init as { duplex: 'half' }).duplex = 'half';
}
return new Request(url, init);
}
/**
* Creates a [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object from the headers in a Node.js
* [`http.IncomingMessage`](https://nodejs.org/api/http.html#class-httpincomingmessage)/[`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#class-http2http2serverrequest).
*
* @param req The incoming request object.
* @returns A headers object.
*/
export function createHeaders(req: http.IncomingMessage | http2.Http2ServerRequest): Headers {
let headers = new Headers();
let rawHeaders = req.rawHeaders;
for (let i = 0; i < rawHeaders.length; i += 2) {
if (rawHeaders[i].startsWith(':')) continue;
headers.append(rawHeaders[i], rawHeaders[i + 1]);
}
return headers;
}
/**
* Sends a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) to the client using a Node.js
* [`http.ServerResponse`](https://nodejs.org/api/http.html#class-httpserverresponse)/[`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#class-http2http2serverresponse)
* object.
*
* @param res The server response object.
* @param response The response to send.
*/
export async function sendResponse(
res: http.ServerResponse | http2.Http2ServerResponse,
response: Response,
): Promise<void> {
// Iterate over response.headers so we are sure to send multiple Set-Cookie headers correctly.
// These would incorrectly be merged into a single header if we tried to use
// `Object.fromEntries(response.headers.entries())`.
let headers: Record<string, string | string[]> = {};
for (let [key, value] of response.headers) {
if (key in headers) {
if (Array.isArray(headers[key])) {
headers[key].push(value);
} else {
headers[key] = [headers[key] as string, value];
}
} else {
headers[key] = value;
}
}
res.writeHead(response.status, headers);
if (response.body != null && res.req.method !== 'HEAD') {
for await (let chunk of readStream(response.body)) {
// @ts-expect-error - Node typings for http2 require a 2nd parameter to write but it's optional
res.write(chunk);
}
}
res.end();
}
export { type ClientAddress, type ErrorHandler, type FetchHandler } from './lib/fetch-handler.ts';
export {
type RequestListenerOptions,
createRequestListener,
type RequestOptions,
createRequest,
createHeaders,
sendResponse,
} from './lib/request-listener.ts';
+5
-2

@@ -35,3 +35,4 @@ "use strict";

const { done, value } = await reader.read();
if (done) break;
if (done)
break;
yield value;

@@ -133,3 +134,4 @@ }

for (let i = 0; i < rawHeaders.length; i += 2) {
if (rawHeaders[i].startsWith(":")) continue;
if (rawHeaders[i].startsWith(":"))
continue;
headers.append(rawHeaders[i], rawHeaders[i + 1]);

@@ -167,1 +169,2 @@ }

});
//# sourceMappingURL=node-fetch-server.cjs.map

@@ -1,130 +0,3 @@

import * as http from 'node:http';
import * as http2 from 'node:http2';
interface ClientAddress {
/**
* The IP address of the client that sent the request.
*
* [Node.js Reference](https://nodejs.org/api/net.html#socketremoteaddress)
*/
address: string;
/**
* The family of the client IP address.
*
* [Node.js Reference](https://nodejs.org/api/net.html#socketremotefamily)
*/
family: 'IPv4' | 'IPv6';
/**
* The remote port of the client that sent the request.
*
* [Node.js Reference](https://nodejs.org/api/net.html#socketremoteport)
*/
port: number;
}
/**
* A function that handles an error that occurred during request handling. May return a response to
* send to the client, or `undefined` to allow the server to send a default error response.
*
* [MDN `Response` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response)
*/
interface ErrorHandler {
(error: unknown): void | Response | Promise<void | Response>;
}
/**
* A function that handles an incoming request and returns a response.
*
* [MDN `Request` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Request)
*
* [MDN `Response` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response)
*/
interface FetchHandler {
(request: Request, client: ClientAddress): Response | Promise<Response>;
}
interface RequestListenerOptions {
/**
* Overrides the host portion of the incoming request URL. By default the request URL host is
* derived from the HTTP `Host` header.
*
* For example, if you have a `$HOST` environment variable that contains the hostname of your
* server, you can use it to set the host of all incoming request URLs like so:
*
* ```ts
* createRequestListener(handler, { host: process.env.HOST })
* ```
*/
host?: string;
/**
* An error handler that determines the response when the request handler throws an error. By
* default a 500 Internal Server Error response will be sent.
*/
onError?: ErrorHandler;
/**
* Overrides the protocol of the incoming request URL. By default the request URL protocol is
* derived from the connection protocol. So e.g. when serving over HTTPS (using
* `https.createServer()`), the request URL will begin with `https:`.
*/
protocol?: string;
}
/**
* Wraps a fetch handler in a Node.js request listener that can be used with:
*
* - [`http.createServer()`](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener)
* - [`https.createServer()`](https://nodejs.org/api/https.html#httpscreateserveroptions-requestlistener)
* - [`http2.createServer()`](https://nodejs.org/api/http2.html#http2createserveroptions-onrequesthandler)
* - [`http2.createSecureServer()`](https://nodejs.org/api/http2.html#http2createsecureserveroptions-onrequesthandler)
*
* Example:
*
* ```ts
* import * as http from 'node:http';
* import { createRequestListener } from '@mjackson/node-fetch-server';
*
* async function handler(request) {
* return new Response('Hello, world!');
* }
*
* let server = http.createServer(
* createRequestListener(handler)
* );
*
* server.listen(3000);
* ```
*
* @param handler The fetch handler to use for processing incoming requests.
* @param options Request listener options.
* @returns A Node.js request listener function.
*/
declare function createRequestListener(handler: FetchHandler, options?: RequestListenerOptions): http.RequestListener;
type RequestOptions = Omit<RequestListenerOptions, 'onError'>;
/**
* Creates a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object from
*
* - a [`http.IncomingMessage`](https://nodejs.org/api/http.html#class-httpincomingmessage)/[`http.ServerResponse`](https://nodejs.org/api/http.html#class-httpserverresponse) pair
* - a [`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#class-http2http2serverrequest)/[`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#class-http2http2serverresponse) pair
*
* @param req The incoming request object.
* @param res The server response object.
* @param options
* @returns A request object.
*/
declare function createRequest(req: http.IncomingMessage | http2.Http2ServerRequest, res: http.ServerResponse | http2.Http2ServerResponse, options?: RequestOptions): Request;
/**
* Creates a [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object from the headers in a Node.js
* [`http.IncomingMessage`](https://nodejs.org/api/http.html#class-httpincomingmessage)/[`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#class-http2http2serverrequest).
*
* @param req The incoming request object.
* @returns A headers object.
*/
declare function createHeaders(req: http.IncomingMessage | http2.Http2ServerRequest): Headers;
/**
* Sends a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) to the client using a Node.js
* [`http.ServerResponse`](https://nodejs.org/api/http.html#class-httpserverresponse)/[`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#class-http2http2serverresponse)
* object.
*
* @param res The server response object.
* @param response The response to send.
*/
declare function sendResponse(res: http.ServerResponse | http2.Http2ServerResponse, response: Response): Promise<void>;
export { type ClientAddress, type ErrorHandler, type FetchHandler, type RequestListenerOptions, type RequestOptions, createHeaders, createRequest, createRequestListener, sendResponse };
export { type ClientAddress, type ErrorHandler, type FetchHandler } from './lib/fetch-handler.ts';
export { type RequestListenerOptions, createRequestListener, type RequestOptions, createRequest, createHeaders, sendResponse, } from './lib/request-listener.ts';
//# sourceMappingURL=node-fetch-server.d.ts.map

@@ -6,3 +6,4 @@ // src/lib/read-stream.ts

const { done, value } = await reader.read();
if (done) break;
if (done)
break;
yield value;

@@ -104,3 +105,4 @@ }

for (let i = 0; i < rawHeaders.length; i += 2) {
if (rawHeaders[i].startsWith(":")) continue;
if (rawHeaders[i].startsWith(":"))
continue;
headers.append(rawHeaders[i], rawHeaders[i + 1]);

@@ -137,1 +139,2 @@ }

};
//# sourceMappingURL=node-fetch-server.js.map
{
"name": "@mjackson/node-fetch-server",
"version": "0.6.1",
"version": "0.7.0",
"description": "Build servers for Node.js using the web fetch API",

@@ -14,27 +14,17 @@ "author": "Michael Jackson <mjijackson@gmail.com>",

"files": [
"LICENSE",
"README.md",
"dist",
"LICENSE",
"README.md"
"src"
],
"type": "module",
"types": "./dist/node-fetch-server.d.ts",
"main": "./dist/node-fetch-server.js",
"module": "./dist/node-fetch-server.js",
"main": "./dist/node-fetch-server.cjs",
"exports": {
".": {
"module-sync": {
"types": "./dist/node-fetch-server.d.ts",
"default": "./dist/node-fetch-server.js"
},
"import": {
"types": "./dist/node-fetch-server.d.ts",
"default": "./dist/node-fetch-server.js"
},
"require": {
"types": "./dist/node-fetch-server.d.cts",
"default": "./dist/node-fetch-server.cjs"
},
"default": {
"types": "./dist/node-fetch-server.d.ts",
"default": "./dist/node-fetch-server.js"
}
"types": "./dist/node-fetch-server.d.ts",
"import": "./dist/node-fetch-server.js",
"require": "./dist/node-fetch-server.cjs",
"default": "./dist/node-fetch-server.js"
},

@@ -45,3 +35,3 @@ "./package.json": "./package.json"

"@types/node": "^22.5.0",
"tsup": "^8.3.5"
"esbuild": "^0.20.2"
},

@@ -58,5 +48,9 @@ "keywords": [

"bench": "bash ./bench/runner.sh",
"build": "tsup",
"build:types": "tsc --project tsconfig.build.json",
"build:esm": "esbuild src/node-fetch-server.ts --bundle --outfile=dist/node-fetch-server.js --format=esm --platform=node --sourcemap",
"build:cjs": "esbuild src/node-fetch-server.ts --bundle --outfile=dist/node-fetch-server.cjs --format=cjs --platform=node --sourcemap",
"build": "pnpm run build:types && pnpm run build:esm && pnpm run build:cjs",
"clean": "rm -rf dist",
"test": "node --experimental-strip-types --disable-warning=ExperimentalWarning --test ./src/**/*.test.ts"
}
}
import * as http from 'node:http';
import * as http2 from 'node:http2';
interface ClientAddress {
/**
* The IP address of the client that sent the request.
*
* [Node.js Reference](https://nodejs.org/api/net.html#socketremoteaddress)
*/
address: string;
/**
* The family of the client IP address.
*
* [Node.js Reference](https://nodejs.org/api/net.html#socketremotefamily)
*/
family: 'IPv4' | 'IPv6';
/**
* The remote port of the client that sent the request.
*
* [Node.js Reference](https://nodejs.org/api/net.html#socketremoteport)
*/
port: number;
}
/**
* A function that handles an error that occurred during request handling. May return a response to
* send to the client, or `undefined` to allow the server to send a default error response.
*
* [MDN `Response` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response)
*/
interface ErrorHandler {
(error: unknown): void | Response | Promise<void | Response>;
}
/**
* A function that handles an incoming request and returns a response.
*
* [MDN `Request` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Request)
*
* [MDN `Response` Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response)
*/
interface FetchHandler {
(request: Request, client: ClientAddress): Response | Promise<Response>;
}
interface RequestListenerOptions {
/**
* Overrides the host portion of the incoming request URL. By default the request URL host is
* derived from the HTTP `Host` header.
*
* For example, if you have a `$HOST` environment variable that contains the hostname of your
* server, you can use it to set the host of all incoming request URLs like so:
*
* ```ts
* createRequestListener(handler, { host: process.env.HOST })
* ```
*/
host?: string;
/**
* An error handler that determines the response when the request handler throws an error. By
* default a 500 Internal Server Error response will be sent.
*/
onError?: ErrorHandler;
/**
* Overrides the protocol of the incoming request URL. By default the request URL protocol is
* derived from the connection protocol. So e.g. when serving over HTTPS (using
* `https.createServer()`), the request URL will begin with `https:`.
*/
protocol?: string;
}
/**
* Wraps a fetch handler in a Node.js request listener that can be used with:
*
* - [`http.createServer()`](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener)
* - [`https.createServer()`](https://nodejs.org/api/https.html#httpscreateserveroptions-requestlistener)
* - [`http2.createServer()`](https://nodejs.org/api/http2.html#http2createserveroptions-onrequesthandler)
* - [`http2.createSecureServer()`](https://nodejs.org/api/http2.html#http2createsecureserveroptions-onrequesthandler)
*
* Example:
*
* ```ts
* import * as http from 'node:http';
* import { createRequestListener } from '@mjackson/node-fetch-server';
*
* async function handler(request) {
* return new Response('Hello, world!');
* }
*
* let server = http.createServer(
* createRequestListener(handler)
* );
*
* server.listen(3000);
* ```
*
* @param handler The fetch handler to use for processing incoming requests.
* @param options Request listener options.
* @returns A Node.js request listener function.
*/
declare function createRequestListener(handler: FetchHandler, options?: RequestListenerOptions): http.RequestListener;
type RequestOptions = Omit<RequestListenerOptions, 'onError'>;
/**
* Creates a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object from
*
* - a [`http.IncomingMessage`](https://nodejs.org/api/http.html#class-httpincomingmessage)/[`http.ServerResponse`](https://nodejs.org/api/http.html#class-httpserverresponse) pair
* - a [`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#class-http2http2serverrequest)/[`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#class-http2http2serverresponse) pair
*
* @param req The incoming request object.
* @param res The server response object.
* @param options
* @returns A request object.
*/
declare function createRequest(req: http.IncomingMessage | http2.Http2ServerRequest, res: http.ServerResponse | http2.Http2ServerResponse, options?: RequestOptions): Request;
/**
* Creates a [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object from the headers in a Node.js
* [`http.IncomingMessage`](https://nodejs.org/api/http.html#class-httpincomingmessage)/[`http2.Http2ServerRequest`](https://nodejs.org/api/http2.html#class-http2http2serverrequest).
*
* @param req The incoming request object.
* @returns A headers object.
*/
declare function createHeaders(req: http.IncomingMessage | http2.Http2ServerRequest): Headers;
/**
* Sends a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) to the client using a Node.js
* [`http.ServerResponse`](https://nodejs.org/api/http.html#class-httpserverresponse)/[`http2.Http2ServerResponse`](https://nodejs.org/api/http2.html#class-http2http2serverresponse)
* object.
*
* @param res The server response object.
* @param response The response to send.
*/
declare function sendResponse(res: http.ServerResponse | http2.Http2ServerResponse, response: Response): Promise<void>;
export { type ClientAddress, type ErrorHandler, type FetchHandler, type RequestListenerOptions, type RequestOptions, createHeaders, createRequest, createRequestListener, sendResponse };