From 9cfc0b630863053df174b7e89f595a6fe57e1481 Mon Sep 17 00:00:00 2001 From: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com> Date: Sat, 13 Jun 2026 01:46:59 -0300 Subject: [PATCH 1/2] fix(producer): guard fileServer.close in distributed cleanup paths `plan()` and `renderChunk()` both close the probe/chunk file server with a bare `fileServer.close()` in their cleanup sequence. `FileServerHandle.close` tears down the underlying http.Server, whose `close()` throws `ERR_SERVER_NOT_RUNNING` if the server was already torn down (for example a cancellation path that closed it once already). An unguarded throw there escapes the cleanup and masks the original plan/render result, exactly the failure the adjacent probe-session close already guards against with a try/catch (its comment even spells this out). Add `closeFileServerSafely`, which wraps the close in a try/catch and logs, and route both cleanup sites through it so the two stay consistent and a throwing close can never mask the real result. Covered by unit tests for both the throwing and happy paths. --- .../producer/src/services/distributed/plan.ts | 3 +- .../src/services/distributed/renderChunk.ts | 9 ++++- .../producer/src/services/fileServer.test.ts | 40 +++++++++++++++++++ packages/producer/src/services/fileServer.ts | 25 ++++++++++++ 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/packages/producer/src/services/distributed/plan.ts b/packages/producer/src/services/distributed/plan.ts index e93dc1f7d..14b23e369 100644 --- a/packages/producer/src/services/distributed/plan.ts +++ b/packages/producer/src/services/distributed/plan.ts @@ -38,6 +38,7 @@ import { join, relative, sep } from "node:path"; import { type CanvasResolution } from "@hyperframes/core"; import { type EngineConfig, getEncoderPreset, resolveConfig } from "@hyperframes/engine"; import { defaultLogger, type ProducerLogger } from "../../logger.js"; +import { closeFileServerSafely } from "../fileServer.js"; import { runAudioStage } from "../render/stages/audioStage.js"; import { runCompileStage } from "../render/stages/compileStage.js"; import { runExtractVideosStage } from "../render/stages/extractVideosStage.js"; @@ -833,7 +834,7 @@ export async function plan( job.duration = probeResult.duration; job.totalFrames = probeResult.totalFrames; const totalFrames = probeResult.totalFrames; - if (probeResult.fileServer) probeResult.fileServer.close(); + if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log); if (probeResult.probeSession) { // Close inside a try/catch — leaking a Chrome process here would mask // the original plan() result on cancellation paths. diff --git a/packages/producer/src/services/distributed/renderChunk.ts b/packages/producer/src/services/distributed/renderChunk.ts index 76cfac21e..3d4b56ded 100644 --- a/packages/producer/src/services/distributed/renderChunk.ts +++ b/packages/producer/src/services/distributed/renderChunk.ts @@ -65,7 +65,12 @@ import { } from "../render/stages/freezePlan.js"; import { sha256Hex } from "../render/stages/planHash.js"; import { applyRuntimeEnvSnapshot } from "../render/runtimeEnvSnapshot.js"; -import { buildVirtualTimeShim, createFileServer, type FileServerHandle } from "../fileServer.js"; +import { + buildVirtualTimeShim, + closeFileServerSafely, + createFileServer, + type FileServerHandle, +} from "../fileServer.js"; import { buildSyntheticRenderJob, type DistributedFormat, @@ -654,7 +659,7 @@ export async function renderChunk( }); } } - fileServer.close(); + closeFileServerSafely(fileServer, "renderChunk", log); // Leave the temp work dir on failure (helps debugging); remove it on // success below. } diff --git a/packages/producer/src/services/fileServer.test.ts b/packages/producer/src/services/fileServer.test.ts index 8172594f8..777f95baa 100644 --- a/packages/producer/src/services/fileServer.test.ts +++ b/packages/producer/src/services/fileServer.test.ts @@ -3,6 +3,7 @@ import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node import path, { join } from "node:path"; import { tmpdir } from "node:os"; import { + closeFileServerSafely, createFileServer, HF_BRIDGE_SCRIPT, HF_EARLY_STUB, @@ -11,6 +12,45 @@ import { VIRTUAL_TIME_SHIM, } from "./fileServer.js"; +function captureLogger() { + const warnings: { message: string; meta?: Record }[] = []; + return { + warnings, + log: { + error() {}, + warn(message: string, meta?: Record) { + warnings.push({ message, meta }); + }, + info() {}, + debug() {}, + }, + }; +} + +describe("closeFileServerSafely", () => { + it("swallows and logs a throwing close instead of propagating", () => { + const { log, warnings } = captureLogger(); + const fileServer = { + close: () => { + // http.Server.close() throws ERR_SERVER_NOT_RUNNING on a second close. + throw new Error("Server is not running."); + }, + }; + expect(() => closeFileServerSafely(fileServer, "plan", log)).not.toThrow(); + expect(warnings).toHaveLength(1); + expect(warnings[0].message).toContain("[plan]"); + expect(warnings[0].meta?.error).toBe("Server is not running."); + }); + + it("closes once and stays quiet on the happy path", () => { + const { log, warnings } = captureLogger(); + let closed = 0; + closeFileServerSafely({ close: () => closed++ }, "renderChunk", log); + expect(closed).toBe(1); + expect(warnings).toHaveLength(0); + }); +}); + describe("injectScriptsIntoHtml", () => { it("injects the virtual time shim into head content before authored scripts", () => { const html = ` diff --git a/packages/producer/src/services/fileServer.ts b/packages/producer/src/services/fileServer.ts index 8b9834696..c2bfa9cf0 100644 --- a/packages/producer/src/services/fileServer.ts +++ b/packages/producer/src/services/fileServer.ts @@ -16,6 +16,7 @@ import { join, extname, resolve, sep } from "node:path"; import { injectScriptsAtHeadStart, injectScriptsIntoHtml } from "@hyperframes/core/compiler"; import { getVerifiedHyperframeRuntimeSource } from "./hyperframeRuntimeLoader.js"; import { getHfEarlyStub } from "../generated/hf-early-stub-inline.js"; +import { defaultLogger, type ProducerLogger } from "../logger.js"; export { injectScriptsAtHeadStart }; @@ -558,6 +559,30 @@ export interface FileServerHandle { addPreHeadScript: (script: string) => void; } +/** + * Close a file server handle, swallowing and logging any error. + * + * `FileServerHandle.close` tears down the underlying http.Server, whose + * `close()` throws `ERR_SERVER_NOT_RUNNING` if the server is already torn down + * (for example a cancellation path that closed it once already). An unguarded + * throw inside a cleanup or `finally` block would mask the original render or + * plan result, so cleanup callers must go through this instead of calling + * `close()` directly. + */ +export function closeFileServerSafely( + fileServer: Pick, + label: string, + log: ProducerLogger = defaultLogger, +): void { + try { + fileServer.close(); + } catch (err) { + log.warn(`[${label}] file server close failed`, { + error: err instanceof Error ? err.message : String(err), + }); + } +} + export function createFileServer(options: FileServerOptions): Promise { const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options; From 38e7eb92e5b513b6ddb2e93205400e8319f166a5 Mon Sep 17 00:00:00 2001 From: Carlos Alcaraz <193642530+calcarazgre646@users.noreply.github.com> Date: Sat, 13 Jun 2026 02:24:56 -0300 Subject: [PATCH 2/2] fix(producer): extend fileServer.close guard to renderOrchestrator success path --- packages/producer/src/services/renderOrchestrator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/producer/src/services/renderOrchestrator.ts b/packages/producer/src/services/renderOrchestrator.ts index 31ff18d4f..a39ca8129 100644 --- a/packages/producer/src/services/renderOrchestrator.ts +++ b/packages/producer/src/services/renderOrchestrator.ts @@ -85,6 +85,7 @@ import { join, dirname, resolve } from "path"; import { randomUUID } from "crypto"; import { fileURLToPath } from "url"; import { + closeFileServerSafely, createFileServer, type FileServerHandle, HF_PAGE_SIDE_COMPOSITING_STUB, @@ -2274,7 +2275,7 @@ export async function executeRenderJob( if (frameLookup) frameLookup.cleanup(); // Stop file server - fileServer.close(); + closeFileServerSafely(fileServer, "renderOrchestrator", log); fileServer = null; // ── Stage 6: Assemble ───────────────────────────────────────────────