Last active
October 16, 2025 07:19
-
-
Save marvinhagemeister/cc236ec97235ce0305ae9d48a24a607d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| }; | |
| } |
For everyone coming across this gist: There are ready made npm packages that are more complete than this gist. You should use these instead:
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
I took a stab at this as well for Mastro: it's in serve.ts – it's mostly adapted from the Astro.js node adapter. And I've been meaning to compare it to the srvx version.