Skip to content

Instantly share code, notes, and snippets.

@marvinhagemeister
Last active October 16, 2025 07:19
Show Gist options
  • Save marvinhagemeister/cc236ec97235ce0305ae9d48a24a607d to your computer and use it in GitHub Desktop.
Save marvinhagemeister/cc236ec97235ce0305ae9d48a24a607d to your computer and use it in GitHub Desktop.
import { ServerResponse, type IncomingMessage } from "node:http";
import { Http2ServerRequest, Http2ServerResponse } from "node:http2";
import { isArrayBufferView } from "node:util/types";
const INTERNAL_BODY = Symbol("internal_body");
const GlobalResponse = Response;
globalThis.Response = class Response extends GlobalResponse {
[INTERNAL_BODY]: BodyInit | null | undefined = null;
constructor(body?: BodyInit | null, init?: ResponseInit) {
super(body, init);
this[INTERNAL_BODY] = body;
}
};
function incomingToRequest(req: IncomingMessage | Http2ServerRequest): Request {
if (req.url === undefined) throw new Error(`Empty request URL`);
const { host } = req.headers;
if (host == undefined) throw new Error(`Missing "Host" header`);
const protocol =
"encrypted" in req.socket && req.socket.encrypted ? "https" : "http";
return new Request(`${protocol}://${host}/${req.url}`, {
method: req.method,
// Node types this as a strongly typed interface
headers: req.headers as Record<string, string>,
});
}
async function applyResponse(
res: Response,
outgoing: ServerResponse | Http2ServerResponse
) {
outgoing.statusCode = res.status;
outgoing.statusMessage = res.statusText;
res.headers.forEach((value, key) => {
outgoing.setHeader(key, value);
});
// deno-lint-ignore no-explicit-any
const body = (res as any)[INTERNAL_BODY];
if (body === null || body === undefined) {
outgoing.writeHead(res.status, res.statusText);
outgoing.end();
} else if (typeof body === "string" || body instanceof Uint8Array) {
outgoing.writeHead(res.status, res.statusText);
outgoing.end(body);
} else if (body instanceof Blob) {
outgoing.writeHead(res.status, res.statusText);
outgoing.end(new Uint8Array(await body.arrayBuffer()));
} else if (body instanceof ArrayBuffer) {
outgoing.writeHead(res.status, res.statusText);
outgoing.end(new Uint8Array(body));
} else if (isArrayBufferView(body)) {
outgoing.writeHead(res.status, res.statusText);
outgoing.end(new Uint8Array(body.buffer));
} else if (body instanceof FormData) {
// TODO
outgoing.setHeader(
"Content-Type",
"application/x-www-form-urlencoded;charset=UTF-8"
);
outgoing.writeHead(res.status, res.statusText);
outgoing.end();
} else {
outgoing.writeHead(res.status, res.statusText);
outgoing.end();
}
}
export function createHttphandler(
handler: (req: Request) => Response | Promise<Response>
) {
return async (
incoming: IncomingMessage | Http2ServerRequest,
outgoing: ServerResponse | Http2ServerResponse
) => {
const req = incomingToRequest(incoming);
const res = await handler(req);
return await applyResponse(res, outgoing);
};
}
@marvinhagemeister
Copy link
Author

For everyone coming across this gist: There are ready made npm packages that are more complete than this gist. You should use these instead:

@mb21
Copy link

mb21 commented Oct 16, 2025

Thanks @marvinhagemeister, I'd love to use a good, minimal npm package that does this. Unfortunately, @remix-run/web-fetch doesn't seem minimal (seems to include a fetch client in addition to the fetch server, and has 8 dependencies). And the source link of @mjackson/node-fetch-server leads to an "archived" repository? :-(

Edit: ah, seems it just moved to the remix-run repo and the proper npm package is now @remix-run/node-fetch-server. Code looks quite good, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment