toNodeListener
Adapts a Fetch handler to Node's http.createServer callback, with streaming, multiple Set-Cookie headers, cancellation on client disconnect and the connection's IP.
import { toNodeListener } from "@meekojs/meeko/node";toNodeListener(handler, options) converts a Fetch handler ((request) => Response, such as app.fetch) to the (req, res) listener for http.createServer or https.createServer. It builds the Request with the public origin's URL, streams the response body, sends each Set-Cookie separately and cancels the body if the client disconnects. It also records the remote socket's IP, read by context.clientAddress, clientAddress(request) and rateLimit.
It is the HTTP part of serveNode. Use it alone to mount Meeko in a custom Node server (direct HTTPS, a shared port, other routes).
Usage#
import { createServer } from "node:http";
import { toNodeListener } from "@meekojs/meeko/node";
const server = createServer(toNodeListener((request) => app.fetch(request), { publicOrigin: "https://example.com" }));
server.listen(3000);Type#
function toNodeListener(
handler: (request: Request) => Promise<Response> | Response,
options?: NodeListenerOptions,
): (request: IncomingMessage, response: ServerResponse) => voidinterface NodeListenerOptions {
onError?: (error: unknown, requestId: string) => void
publicOrigin?: string
}Parameters#
| Parameter | Type | Default | Description |
|---|---|---|---|
handler |
(request: Request) => Response | Promise<Response> |
required | The Fetch handler. |
options.publicOrigin |
string |
the Host header and socket protocol |
Origin used to build URLs, such as https://example.com behind a TLS proxy. An absolute-form request to another origin receives 400. |
options.onError |
(error, requestId) => void |
console | Receives handler or response stream failures, with a correlation ID. |
Return value#
The (request: IncomingMessage, response: ServerResponse) => void listener. It never throws: if the handler rejects before headers are sent, it responds with errorResponse (500 INTERNAL_ERROR, or an ApiError's status) and the same requestId received by onError; if sending has already started, it terminates the connection.
Examples#
Direct HTTPS, without a proxy#
import { readFileSync } from "node:fs";
import { createServer } from "node:https";
import { attachNodeWebSocketServer, toNodeListener } from "@meekojs/meeko/node";
const server = createServer(
{ key: readFileSync("/etc/tls/key.pem"), cert: readFileSync("/etc/tls/cert.pem") },
toNodeListener((request) => app.fetch(request), { publicOrigin: "https://example.com", onError: (error, id) => app.reportError(error, id) }),
);
if (app.socket) attachNodeWebSocketServer({ server, hub: app.socket.hub, publicOrigin: "https://example.com", pathnamePrefix: app.socket.pathnamePrefix });
server.listen(443);Meeko alongside another application#
import { createServer } from "node:http";
import { toNodeListener } from "@meekojs/meeko/node";
const meeko = toNodeListener((request) => app.fetch(request), { publicOrigin: "https://example.com" });
createServer((req, res) => {
if (req.url?.startsWith("/legacy/")) return legacyHandler(req, res);
meeko(req, res);
}).listen(3000);Any Fetch handler#
import { createServer } from "node:http";
import { toNodeListener } from "@meekojs/meeko/node";
export const server = createServer(toNodeListener((request) => new Response(`Hello from ${new URL(request.url).pathname}`)));Common mistakes#
A public origin without a scheme#
toNodeListener(handler, { publicOrigin: "example.com" });TypeError: Invalid publicOrigin: example.com, when creating the listener. With another scheme, publicOrigin must use HTTP or HTTPS.
toNodeListener(handler, { publicOrigin: "https://example.com" });Trust the Host header behind a proxy#
createServer(toNodeListener((request) => app.fetch(request))); // behind a TLS proxyWithout publicOrigin, the URL comes from Host and the socket (http://), so the application does not recognize the browser's https:// Origin and responds to mutations with 403 INVALID_ORIGIN. createServerApp also accepts publicOrigin or MEEKO_PUBLIC_ORIGIN.
createServer(toNodeListener((request) => app.fetch(request), { publicOrigin: process.env.MEEKO_PUBLIC_ORIGIN }));Forget Socket#
createServer(toNodeListener((request) => app.fetch(request))).listen(3000); // the application has server/socketThere is no startup error, but nothing handles upgrades and the client retries forever. toNodeListener only handles HTTP.
const server = createServer(toNodeListener((request) => app.fetch(request), { publicOrigin }));
if (app.socket) attachNodeWebSocketServer({ server, hub: app.socket.hub, publicOrigin, pathnamePrefix: app.socket.pathnamePrefix });Notes#
- Cancellation. If the client closes the connection, the request's
signalis aborted and the response body is canceled: files are released and stream producers stop. HEAD. Sends headers without a body.- Request-target. A target starting with
//is always a path on the application's own origin, never another host. - IP. Without
MEEKO_TRUST_PROXY, the IP is the socket's;X-Forwarded-Foris not read. - Platform.
toNodeListenerdoes not passplatformtoapp.fetch: with a custom host,waitUntilandenvare those received bycreateServerApp;serveNodealso waits forwaitUntiltasks when shutting down.