Skip to content

Instantly share code, notes, and snippets.

@cliffordfajardo
Forked from jacob-ebey/fetch-server.d.ts
Created August 23, 2023 18:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cliffordfajardo/a2dba7f4bb6c05edc6a04eff3804c647 to your computer and use it in GitHub Desktop.
Save cliffordfajardo/a2dba7f4bb6c05edc6a04eff3804c647 to your computer and use it in GitHub Desktop.
Node Fetch Server
import type { Server } from "node:http";
export type Handler = (request: Request) => Response | Promise<Response>;
export type CreateServerOptions = {
onError?: (error: unknown) => void;
};
export declare function createServer(
handler: Handler,
options?: CreateServerOptions,
): Server;
import * as http from "node:http";
import * as stream from "node:stream";
/** @type {import("./fetch-server.js").createServer} */
export function createServer(handler, options) {
const onError = options?.onError ?? console.error;
return http.createServer(async (req, res) => {
const request = createRequest(req, res);
try {
const response = await handler(request);
sendResponse(response, res);
} catch (error) {
onError(error);
if (!res.headersSent) {
res.writeHead(500);
}
try {
res.end();
} catch {
// ignore
}
}
});
}
/**
* @param {http.IncomingMessage} req
* @param {http.ServerResponse} res
* @returns {Request}
*/
function createRequest(req, res) {
const method = req.method;
const normalizedMethod = method.toUpperCase();
const schema = req.headers["x-forwarded-proto"] || "http";
const host = req.headers["x-forwarded-host"] || req.headers.host;
const url = new URL(`${schema}://${host}${req.url ?? "/"}`);
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (Array.isArray(value)) {
for (const item of value) {
headers.append(key, item);
}
} else {
headers.append(key, value);
}
}
const controller = new AbortController();
const signal = controller.signal;
res.once("close", () => controller.abort());
/** @type {RequestInit & { duplex?: "half" }} */
const init = {
headers,
method,
signal,
};
if (normalizedMethod !== "GET" && normalizedMethod !== "HEAD") {
init.body =
/** @type {ReadableStream<Uint8Array>} */ (stream.Readable.toWeb(req));
init.duplex = "half";
}
return new Request(url.href, init);
}
/**
* @param {Response} response
* @param {http.ServerResponse} res
*/
function sendResponse(response, res) {
/** @type {http.OutgoingHttpHeaders} */
const headers = {};
for (const [key, value] of response.headers) {
const existing = headers[key];
if (Array.isArray(existing)) {
existing.push(value);
} else if (typeof existing === "string") {
headers[key] = [existing, value];
} else {
headers[key] = value;
}
}
res.writeHead(response.status, response.statusText, headers);
if (response.body) {
stream.Readable.fromWeb(
/** @type {import("node:stream/web").ReadableStream} */ (response.body),
).pipe(res, { end: true });
} else {
res.end();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment