Skip to content

Instantly share code, notes, and snippets.

@marvinhagemeister
Last active February 12, 2025 08:34
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);
};
}
@ksmithut
Copy link

ksmithut commented Feb 1, 2025

If you switch the outgoing.setHeader(key, value); to outgoing.appendHeader(key, value);, it will work with the Set-Cookie header. If you want to set multiple cookies on the same response, you need to send multiple Set-Cookie key/pair headers. I've just tried it out in node, and it works if you swap out the setHeader call to appendHeader. It appears they have a special case for the set-cookie header 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:

import stream from 'node:stream'

// ...

stream.Readable.fromWeb(res.body).pipe(outgoing)

It converts the Response body (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 fetch server like Deno and Bun for a while and your article gives me hope that this will become a standard across all JavaScript server runtimes.

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