From 67342bdd4595cf4b6787fc9dd7abc536bd4c11f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 31 Mar 2026 20:47:23 +0200 Subject: [PATCH] fix(producer): force-close file server connections after render The render process hung after completion because the file server's HTTP keep-alive connections held the Node.js event loop open. server.close() stops accepting new connections but doesn't destroy existing ones. Now tracks all connections and destroys them on close. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/commands/render.ts | 4 ---- packages/producer/src/services/fileServer.ts | 20 +++++++++++++++++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts index f2f1ba7ed3..4bc73211cc 100644 --- a/packages/cli/src/commands/render.ts +++ b/packages/cli/src/commands/render.ts @@ -253,10 +253,6 @@ Examples: browserPath, }); } - - // Explicit exit — Puppeteer/Chrome CDP connections hold the event loop - // open even after all work is done. Without this, the process hangs. - process.exit(0); }, }); diff --git a/packages/producer/src/services/fileServer.ts b/packages/producer/src/services/fileServer.ts index 03b62f0710..d6af58fff8 100644 --- a/packages/producer/src/services/fileServer.ts +++ b/packages/producer/src/services/fileServer.ts @@ -9,6 +9,7 @@ import { Hono } from "hono"; import { serve } from "@hono/node-server"; +import type { IncomingMessage } from "node:http"; import { readFileSync, existsSync, statSync } from "node:fs"; import { join, extname } from "node:path"; import { getVerifiedHyperframeRuntimeSource } from "./hyperframeRuntimeLoader.js"; @@ -323,14 +324,31 @@ export function createFileServer(options: FileServerOptions): Promise { + // Track open connections so we can force-destroy them on close. + // Without this, server.close() waits for keep-alive connections to + // drain, holding the Node.js event loop open indefinitely. + const connections = new Set(); const server = serve({ fetch: app.fetch, port }, (info) => { const actualPort = info.port; const url = `http://localhost:${actualPort}`; + const httpServer = (server as unknown as { server?: { on: Function } }).server; + if (httpServer?.on) { + httpServer.on("connection", (socket: IncomingMessage["socket"]) => { + connections.add(socket); + socket.on("close", () => connections.delete(socket)); + }); + } + resolve({ url, port: actualPort, - close: () => server.close(), + close: () => { + // Destroy all open connections, then close the server + for (const socket of connections) socket.destroy(); + connections.clear(); + server.close(); + }, }); }); });