-
-
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); | |
| }; | |
| } |
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.
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!
If you switch the
outgoing.setHeader(key, value);tooutgoing.appendHeader(key, value);, it will work with theSet-Cookieheader. If you want to set multiple cookies on the same response, you need to send multipleSet-Cookiekey/pair headers. I've just tried it out in node, and it works if you swap out thesetHeadercall toappendHeader. It appears they have a special case for theset-cookieheader where it will return multiple values in the iterator.It also looks like you have a lot of different handlers for different response body types. That seems like it would be the most performant solution. I also found that you could handle all those cases (possibly? I didn't test them all) by using a function from node's stream api:
It converts the
Responsebody (which is a Web ReadableStream) into a node stream, then pipes it to the node ServerResponse, which is also a writeable stream.Would love your thoughts! I've been wanting node to implement the
fetchserver like Deno and Bun for a while and your article gives me hope that this will become a standard across all JavaScript server runtimes.