Last active
February 7, 2025 19:12
-
-
Save marvinhagemeister/cc236ec97235ce0305ae9d48a24a607d to your computer and use it in GitHub Desktop.
This file contains 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); | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If you switch the
outgoing.setHeader(key, value);
tooutgoing.appendHeader(key, value);
, it will work with theSet-Cookie
header. If you want to set multiple cookies on the same response, you need to send multipleSet-Cookie
key/pair headers. I've just tried it out in node, and it works if you swap out thesetHeader
call toappendHeader
. It appears they have a special case for theset-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:
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.