diff --git a/packages/engine/src/services/fileServer.file-race.test.ts b/packages/engine/src/services/fileServer.file-race.test.ts new file mode 100644 index 0000000000..c934e2292e --- /dev/null +++ b/packages/engine/src/services/fileServer.file-race.test.ts @@ -0,0 +1,212 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import * as fs from "node:fs"; +import { execFileSync } from "node:child_process"; +import { createServer } from "node:net"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createFileServer, type FileServerHandle } from "./fileServer.js"; + +const hooks = vi.hoisted(() => ({ + checked: (_path: fs.PathLike) => {}, + beforeRead: () => {}, + opened: new Map(), + active: new Set(), +})); + +vi.mock("node:fs", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + openSync: (path: fs.PathLike, flags: number | string) => { + const fd = actual.openSync(path, flags); + hooks.opened.set(fd, path); + hooks.active.add(fd); + return fd; + }, + closeSync: (fd: number) => { + actual.closeSync(fd); + hooks.active.delete(fd); + }, + statSync: (path: fs.PathLike) => { + const stat = actual.statSync(path); + hooks.checked(path); + return stat; + }, + fstatSync: (fd: number) => { + const stat = actual.fstatSync(fd); + const path = hooks.opened.get(fd); + if (path) hooks.checked(path); + return stat; + }, + readFileSync: (path: fs.PathOrFileDescriptor, encoding?: BufferEncoding) => { + hooks.beforeRead(); + return encoding ? actual.readFileSync(path, encoding) : actual.readFileSync(path); + }, + }; +}); + +describe("file server checked reads", () => { + let root: string; + let projectDir: string; + let compiledDir: string; + let server: FileServerHandle | undefined; + + beforeEach(() => { + root = fs.mkdtempSync(join(tmpdir(), "hf-server-read-")); + projectDir = join(root, "project"); + compiledDir = join(root, "compiled"); + fs.mkdirSync(projectDir); + fs.mkdirSync(compiledDir); + hooks.opened.clear(); + hooks.active.clear(); + }); + + afterEach(() => { + server?.close(); + server = undefined; + hooks.checked = () => {}; + hooks.beforeRead = () => {}; + vi.restoreAllMocks(); + fs.rmSync(root, { recursive: true, force: true }); + }); + + function expectClosed() { + // HTTP sockets can reuse an already-closed fd before fetch resolves. + // Track successful real closes instead of probing stale descriptor numbers. + expect(hooks.opened.size).toBeGreaterThan(0); + expect(hooks.active.size).toBe(0); + } + + it.each([ + ["project", "html"], + ["project", "bin"], + ["compiled", "html"], + ["compiled", "bin"], + ])("reads the checked %s .%s despite replacement", async (source, ext) => { + const name = `asset.${ext}`; + const file = join(source === "compiled" ? compiledDir : projectDir, name); + fs.writeFileSync(join(projectDir, name), "project bytes"); + fs.writeFileSync(file, "checked bytes"); + hooks.checked = (path) => { + if (path !== file) return; + hooks.checked = () => {}; + fs.renameSync(file, join(root, "original")); + fs.writeFileSync(file, "replacement bytes"); + }; + server = await createFileServer({ projectDir, compiledDir }); + const response = await fetch(`${server.url}/${name}`); + expect(response.status).toBe(200); + expect(await response.text()).toBe("checked bytes"); + expectClosed(); + }); + + it.each(["missing", "directory", "non-directory parent"])( + "falls back from a compiled %s", + async (kind) => { + fs.mkdirSync(join(projectDir, "assets")); + fs.writeFileSync(join(projectDir, "assets", "file.bin"), "project bytes"); + if (kind === "directory") + fs.mkdirSync(join(compiledDir, "assets", "file.bin"), { recursive: true }); + if (kind === "non-directory parent") fs.writeFileSync(join(compiledDir, "assets"), "blocker"); + server = await createFileServer({ projectDir, compiledDir }); + const response = await fetch(`${server.url}/assets/file.bin`); + expect(response.status).toBe(200); + expect(await response.text()).toBe("project bytes"); + expectClosed(); + }, + ); + + it.skipIf(process.platform === "win32")("skips a compiled FIFO without blocking", async () => { + fs.writeFileSync(join(projectDir, "file.bin"), "project bytes"); + execFileSync("mkfifo", [join(compiledDir, "file.bin")]); + server = await createFileServer({ projectDir, compiledDir }); + const response = await fetch(`${server.url}/file.bin`); + expect(await response.text()).toBe("project bytes"); + expectClosed(); + }); + + it.skipIf(process.platform === "win32")("falls back from a compiled Unix socket", async () => { + fs.writeFileSync(join(projectDir, "file.bin"), "project bytes"); + const socket = createServer(); + await new Promise((resolve) => socket.listen(join(compiledDir, "file.bin"), resolve)); + try { + server = await createFileServer({ projectDir, compiledDir }); + const response = await fetch(`${server.url}/file.bin`); + expect(response.status).toBe(200); + expect(await response.text()).toBe("project bytes"); + expectClosed(); + } finally { + await new Promise((resolve, reject) => + socket.close((error) => (error ? reject(error) : resolve())), + ); + } + }); + + it("serves an empty compiled file instead of falling back", async () => { + fs.writeFileSync(join(projectDir, "file.bin"), "project bytes"); + fs.writeFileSync(join(compiledDir, "file.bin"), ""); + server = await createFileServer({ projectDir, compiledDir }); + const response = await fetch(`${server.url}/file.bin`); + expect(response.status).toBe(200); + expect(await response.text()).toBe(""); + expectClosed(); + }); + + it("continues serving symlinked assets", async () => { + fs.writeFileSync(join(projectDir, "file.bin"), "project bytes"); + fs.symlinkSync( + projectDir, + join(compiledDir, "assets"), + process.platform === "win32" ? "junction" : "dir", + ); + server = await createFileServer({ projectDir, compiledDir }); + const response = await fetch(`${server.url}/assets/file.bin`); + expect(response.status).toBe(200); + expect(await response.text()).toBe("project bytes"); + expectClosed(); + }); + + it("keeps missing files and directories at 404", async () => { + fs.mkdirSync(join(projectDir, "folder")); + server = await createFileServer({ projectDir }); + for (const path of ["missing", "folder", "missing/child"]) { + const response = await fetch(`${server.url}/${path}`); + expect(response.status).toBe(404); + expect(await response.text()).toBe("Not found"); + } + }); + + it("injects scripts only into the selected index HTML", async () => { + const html = "compiled"; + fs.writeFileSync(join(projectDir, "index.html"), "project"); + fs.writeFileSync(join(compiledDir, "index.html"), html); + fs.writeFileSync(join(compiledDir, "other.html"), html); + server = await createFileServer({ + projectDir, + compiledDir, + headScripts: ["window.headTest = 1;"], + bodyScripts: ["window.bodyTest = 1;"], + }); + const response = await fetch(server.url); + expect(response.headers.get("content-type")).toBe("text/html; charset=utf-8"); + const text = await response.text(); + expect(text).toContain("compiled"); + expect(text).toContain("window.headTest = 1;"); + expect(text).toContain("window.bodyTest = 1;"); + expect(await (await fetch(`${server.url}/other.html`)).text()).toBe(html); + expectClosed(); + }); + + it.each(["stat", "read"])("closes the selected file when %s fails", async (step) => { + fs.writeFileSync(join(projectDir, "file.bin"), "bytes"); + const fail = () => { + throw new Error("Injected failure"); + }; + if (step === "stat") hooks.checked = fail; + else hooks.beforeRead = fail; + vi.spyOn(console, "error").mockImplementation(() => {}); + server = await createFileServer({ projectDir }); + expect((await fetch(`${server.url}/file.bin`)).status).toBe(500); + expectClosed(); + }); +}); diff --git a/packages/engine/src/services/fileServer.ts b/packages/engine/src/services/fileServer.ts index f3e8cdc61a..04b4a62fe2 100644 --- a/packages/engine/src/services/fileServer.ts +++ b/packages/engine/src/services/fileServer.ts @@ -8,7 +8,7 @@ import { Hono } from "hono"; import { serve } from "@hono/node-server"; -import { readFileSync, existsSync, statSync } from "node:fs"; +import { readFileSync, openSync, fstatSync, closeSync, statSync, constants } from "node:fs"; import { join, extname } from "node:path"; import { injectScriptsIntoHtml } from "@hyperframes/core/compiler"; @@ -59,6 +59,31 @@ export interface FileServerHandle { close: () => void; } +function readRegularFile(filePath: string): Buffer | null { + let fd: number; + try { + // Do not block on a named pipe before fstat can reject non-regular files. + fd = openSync(filePath, constants.O_RDONLY | constants.O_NONBLOCK); + } catch (error) { + if ( + error instanceof Error && + "code" in error && + (error.code === "ENOENT" || error.code === "ENOTDIR") + ) + return null; + // Platforms report different open errors for directories and sockets. + // This check only classifies a failed open; no read follows it. + if (!statSync(filePath, { throwIfNoEntry: false })?.isFile()) return null; + throw error; + } + try { + if (!fstatSync(fd).isFile()) return null; + return readFileSync(fd); + } finally { + closeSync(fd); + } +} + export function createFileServer(options: FileServerOptions): Promise { const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options; @@ -74,20 +99,16 @@ export function createFileServer(options: FileServerOptions): Promise ({ + checked: (_path: fs.PathLike) => {}, + beforeRead: () => {}, + opened: new Map(), + active: new Set(), + failStream: false, + failCreation: false, + streams: new Array(), +})); + +vi.mock("node:fs", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + openSync: (path: fs.PathLike, flags: number | string) => { + const fd = actual.openSync(path, flags); + hooks.opened.set(fd, path); + hooks.active.add(fd); + return fd; + }, + closeSync: (fd: number) => { + actual.closeSync(fd); + hooks.active.delete(fd); + }, + statSync: (path: fs.PathLike) => { + const stat = actual.statSync(path); + hooks.checked(path); + return stat; + }, + fstatSync: (fd: number) => { + const stat = actual.fstatSync(fd); + const path = hooks.opened.get(fd); + if (path) hooks.checked(path); + return stat; + }, + readFile: ( + path: fs.PathOrFileDescriptor, + encoding: BufferEncoding, + callback: (error: NodeJS.ErrnoException | null, data?: string) => void, + ) => { + try { + hooks.beforeRead(); + } catch (error) { + callback(error instanceof Error ? error : new Error(String(error))); + return; + } + actual.readFile(path, encoding, callback); + }, + createReadStream: (path: fs.PathLike, options?: Parameters[1]) => { + if (hooks.failCreation) throw new Error("Injected stream construction failure"); + const stream = actual.createReadStream(path, options); + hooks.streams.push(stream); + const fd = typeof options === "object" ? options.fd : undefined; + if (typeof fd === "number") stream.on("close", () => hooks.active.delete(fd)); + if (hooks.failStream) + queueMicrotask(() => stream.destroy(new Error("Injected stream failure"))); + return stream; + }, + }; +}); + +describe("producer file server checked reads", () => { + let root: string; + let projectDir: string; + let compiledDir: string; + let server: FileServerHandle | undefined; + + beforeEach(() => { + root = fs.mkdtempSync(join(tmpdir(), "hf-server-read-")); + projectDir = join(root, "project"); + compiledDir = join(root, "compiled"); + fs.mkdirSync(projectDir); + fs.mkdirSync(compiledDir); + hooks.opened.clear(); + hooks.active.clear(); + hooks.streams = []; + hooks.failStream = false; + hooks.failCreation = false; + }); + + afterEach(() => { + server?.close(); + server = undefined; + hooks.checked = () => {}; + hooks.beforeRead = () => {}; + vi.restoreAllMocks(); + fs.rmSync(root, { recursive: true, force: true }); + }); + + async function expectClosed() { + // HTTP sockets can reuse an already-closed fd before fetch resolves. + // Track successful real closes instead of probing stale descriptor numbers. + expect(hooks.opened.size).toBeGreaterThan(0); + await vi.waitFor(() => expect(hooks.active.size).toBe(0)); + } + + it.each([ + ["project", "html"], + ["project", "bin"], + ["project", "range"], + ["compiled", "html"], + ["compiled", "bin"], + ["compiled", "range"], + ])("reads the checked %s .%s despite replacement", async (source, ext) => { + const name = `asset.${ext === "range" ? "bin" : ext}`; + const file = join(source === "compiled" ? compiledDir : projectDir, name); + fs.writeFileSync(join(projectDir, name), "project bytes"); + fs.writeFileSync(file, "checked bytes"); + hooks.checked = (path) => { + if (path !== file) return; + hooks.checked = () => {}; + fs.renameSync(file, join(root, "original")); + fs.writeFileSync(file, "replacement bytes"); + }; + server = await createFileServer({ headScripts: [], bodyScripts: [], projectDir, compiledDir }); + const response = await fetch( + `${server.url}/${name}`, + ext === "range" ? { headers: { Range: "bytes=1-4" } } : {}, + ); + expect(response.status).toBe(ext === "range" ? 206 : 200); + if (ext === "range") { + expect(response.headers.get("content-range")).toBe("bytes 1-4/13"); + expect(await response.text()).toBe("heck"); + } else { + expect(await response.text()).toContain("checked bytes"); + if (ext === "bin") expect(response.headers.get("content-length")).toBe("13"); + } + await expectClosed(); + }); + + it.each(["missing", "directory", "non-directory parent"])( + "falls back from a compiled %s", + async (kind) => { + fs.mkdirSync(join(projectDir, "assets")); + fs.writeFileSync(join(projectDir, "assets", "file.bin"), "project bytes"); + if (kind === "directory") + fs.mkdirSync(join(compiledDir, "assets", "file.bin"), { recursive: true }); + if (kind === "non-directory parent") fs.writeFileSync(join(compiledDir, "assets"), "blocker"); + server = await createFileServer({ + headScripts: [], + bodyScripts: [], + projectDir, + compiledDir, + }); + const response = await fetch(`${server.url}/assets/file.bin`); + expect(response.status).toBe(200); + expect(await response.text()).toBe("project bytes"); + await expectClosed(); + }, + ); + + it.skipIf(process.platform === "win32")("skips a compiled FIFO without blocking", async () => { + fs.writeFileSync(join(projectDir, "file.bin"), "project bytes"); + execFileSync("mkfifo", [join(compiledDir, "file.bin")]); + server = await createFileServer({ headScripts: [], bodyScripts: [], projectDir, compiledDir }); + const response = await fetch(`${server.url}/file.bin`); + expect(await response.text()).toBe("project bytes"); + await expectClosed(); + }); + + it.skipIf(process.platform === "win32")("falls back from a compiled Unix socket", async () => { + fs.writeFileSync(join(projectDir, "file.bin"), "project bytes"); + const socket = createServer(); + await new Promise((resolve) => socket.listen(join(compiledDir, "file.bin"), resolve)); + try { + server = await createFileServer({ + headScripts: [], + bodyScripts: [], + projectDir, + compiledDir, + }); + const response = await fetch(`${server.url}/file.bin`); + expect(response.status).toBe(200); + expect(await response.text()).toBe("project bytes"); + await expectClosed(); + } finally { + await new Promise((resolve, reject) => + socket.close((error) => (error ? reject(error) : resolve())), + ); + } + }); + + it("serves an empty compiled file instead of falling back", async () => { + fs.writeFileSync(join(projectDir, "file.bin"), "project bytes"); + fs.writeFileSync(join(compiledDir, "file.bin"), ""); + server = await createFileServer({ headScripts: [], bodyScripts: [], projectDir, compiledDir }); + const response = await fetch(`${server.url}/file.bin`); + expect(response.status).toBe(200); + expect(await response.text()).toBe(""); + await expectClosed(); + }); + + it("continues serving symlinked assets", async () => { + fs.writeFileSync(join(projectDir, "file.bin"), "project bytes"); + fs.symlinkSync( + projectDir, + join(compiledDir, "assets"), + process.platform === "win32" ? "junction" : "dir", + ); + server = await createFileServer({ headScripts: [], bodyScripts: [], projectDir, compiledDir }); + const response = await fetch(`${server.url}/assets/file.bin`); + expect(response.status).toBe(200); + expect(await response.text()).toBe("project bytes"); + await expectClosed(); + }); + + it("keeps missing files and directories at 404", async () => { + fs.mkdirSync(join(projectDir, "folder")); + server = await createFileServer({ headScripts: [], bodyScripts: [], projectDir }); + for (const path of ["missing", "folder", "missing/child"]) { + const response = await fetch(`${server.url}/${path}`); + expect(response.status).toBe(404); + expect(await response.text()).toBe("Not found"); + } + }); + + it("injects scripts only into the selected index HTML", async () => { + const html = "compiled"; + fs.writeFileSync(join(projectDir, "index.html"), "project"); + fs.writeFileSync(join(compiledDir, "index.html"), html); + fs.writeFileSync(join(compiledDir, "other.html"), html); + server = await createFileServer({ + projectDir, + compiledDir, + headScripts: ["window.headTest = 1;"], + bodyScripts: ["window.bodyTest = 1;"], + }); + const response = await fetch(server.url); + expect(response.headers.get("content-type")).toBe("text/html; charset=utf-8"); + const text = await response.text(); + expect(text).toContain("compiled"); + expect(text).toContain("window.headTest = 1;"); + expect(text).toContain("window.bodyTest = 1;"); + const other = await (await fetch(`${server.url}/other.html`)).text(); + expect(other).toContain("compiled"); + expect(other).not.toContain("window.headTest = 1;"); + expect(other).not.toContain("window.bodyTest = 1;"); + await expectClosed(); + }); + + it.each(["full", "range"])("closes a %s stream after a read error", async (kind) => { + fs.writeFileSync(join(projectDir, "file.bin"), "checked bytes"); + hooks.failStream = true; + vi.spyOn(console, "error").mockImplementation(() => {}); + server = await createFileServer({ headScripts: [], bodyScripts: [], projectDir }); + const url = server.url; + await expect(async () => { + const response = await fetch( + `${url}/file.bin`, + kind === "range" ? { headers: { Range: "bytes=1-4" } } : {}, + ); + await response.arrayBuffer(); + }).rejects.toThrow(); + await expectClosed(); + }); + + it.each(["creation", "conversion"])( + "closes the descriptor after stream %s fails", + async (stage) => { + fs.writeFileSync(join(projectDir, "file.bin"), "checked bytes"); + if (stage === "creation") hooks.failCreation = true; + else + vi.spyOn(Readable, "toWeb").mockImplementationOnce(() => { + throw new Error("Injected conversion failure"); + }); + vi.spyOn(console, "error").mockImplementation(() => {}); + server = await createFileServer({ headScripts: [], bodyScripts: [], projectDir }); + expect((await fetch(`${server.url}/file.bin`)).status).toBe(500); + await expectClosed(); + }, + ); + + it.each(["full", "range"])("closes a %s stream when the client cancels", async (kind) => { + const file = join(projectDir, "file.bin"); + fs.writeFileSync(file, ""); + fs.truncateSync(file, 32 * 1024 * 1024); + server = await createFileServer({ headScripts: [], bodyScripts: [], projectDir }); + const response = await fetch( + `${server.url}/file.bin`, + kind === "range" ? { headers: { Range: "bytes=1-" } } : {}, + ); + expect(response.status).toBe(kind === "range" ? 206 : 200); + await response.body?.cancel(); + await expectClosed(); + }); + + it.each(["full", "range", "unsatisfiable"])( + "closes a %s HEAD response without creating a stream", + async (kind) => { + fs.writeFileSync(join(projectDir, "file.bin"), "checked bytes"); + server = await createFileServer({ headScripts: [], bodyScripts: [], projectDir }); + const response = await fetch(`${server.url}/file.bin`, { + method: "HEAD", + headers: kind === "full" ? {} : { Range: kind === "range" ? "bytes=1-4" : "bytes=99-" }, + }); + expect(response.status).toBe(kind === "full" ? 200 : kind === "range" ? 206 : 416); + expect(await response.text()).toBe(""); + expect(hooks.streams).toHaveLength(0); + await expectClosed(); + }, + ); + + it("closes an unsatisfiable GET without creating a stream", async () => { + fs.writeFileSync(join(projectDir, "file.bin"), "checked bytes"); + server = await createFileServer({ headScripts: [], bodyScripts: [], projectDir }); + const response = await fetch(`${server.url}/file.bin`, { headers: { Range: "bytes=99-" } }); + expect(response.status).toBe(416); + expect(response.headers.get("content-range")).toBe("bytes */13"); + expect(hooks.streams).toHaveLength(0); + await expectClosed(); + }); + + it.each(["stat", "read"])("closes the selected file when %s fails", async (step) => { + fs.writeFileSync(join(projectDir, "file.html"), "bytes"); + const fail = () => { + throw new Error("Injected failure"); + }; + if (step === "stat") hooks.checked = fail; + else hooks.beforeRead = fail; + vi.spyOn(console, "error").mockImplementation(() => {}); + server = await createFileServer({ headScripts: [], bodyScripts: [], projectDir }); + expect((await fetch(`${server.url}/file.html`)).status).toBe(500); + await expectClosed(); + }); +}); diff --git a/packages/producer/src/services/fileServer.ts b/packages/producer/src/services/fileServer.ts index 33ceade0b9..24d4136952 100644 --- a/packages/producer/src/services/fileServer.ts +++ b/packages/producer/src/services/fileServer.ts @@ -11,8 +11,19 @@ import { Hono } from "hono"; import { serve } from "@hono/node-server"; import type { IncomingMessage } from "node:http"; -import { existsSync, realpathSync, statSync, createReadStream } from "node:fs"; -import { readFile } from "node:fs/promises"; +import { + existsSync, + realpathSync, + statSync, + createReadStream, + openSync, + fstatSync, + closeSync, + constants, + readFile, +} from "node:fs"; +import { promisify } from "node:util"; + import { Readable } from "node:stream"; import { join, extname, resolve, sep } from "node:path"; import { injectScriptsAtHeadStart, injectScriptsIntoHtml } from "@hyperframes/core/compiler"; @@ -21,6 +32,8 @@ import { getVerifiedHyperframeRuntimeSource } from "./hyperframeRuntimeLoader.js import { getHfEarlyStub } from "../generated/hf-early-stub-inline.js"; import { defaultLogger, type ProducerLogger } from "../logger.js"; +const readFileAsync = promisify(readFile); + export { injectScriptsAtHeadStart }; type PathModuleLike = { @@ -714,6 +727,34 @@ export function closeFileServerSafely( } } +function openRegularFile(filePath: string) { + let fd: number; + try { + // Reject FIFOs with fstat without waiting for a writer to connect. + fd = openSync(filePath, constants.O_RDONLY | constants.O_NONBLOCK); + } catch (error) { + if ( + error instanceof Error && + "code" in error && + (error.code === "ENOENT" || error.code === "ENOTDIR") + ) + return null; + // A failed open of a directory/socket has platform-specific errors. + // Classifying it here never authorizes a later pathname read. + if (!statSync(filePath, { throwIfNoEntry: false })?.isFile()) return null; + throw error; + } + let accepted = false; + try { + const stat = fstatSync(fd); + if (!stat.isFile()) return null; + accepted = true; + return { fd, stat, filePath }; + } finally { + if (!accepted) closeSync(fd); + } +} + export function createFileServer(options: FileServerOptions): Promise { const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options; @@ -757,118 +798,105 @@ export function createFileServer(options: FileServerOptions): Promise = null; + for (const root of compiledDir ? [compiledDir, projectDir] : [projectDir]) { + const candidate = join(root, relativePath); + if (isPathInside(candidate, root)) file = openRegularFile(candidate); + if (file) break; } - if (!filePath) { + if (!file) { if (!/favicon\.ico$/i.test(requestPath)) { console.warn(`[FileServer] 404 Not Found: ${requestPath}`); } return c.text("Not found", 404); } - const ext = extname(filePath).toLowerCase(); - const contentType = MIME_TYPES[ext] || "application/octet-stream"; - - if (ext === ".html") { - // Use the async read here so we don't block the Node event loop while - // reading an HTML file (typically small, but a 200KB+ AI-generated - // composition during a concurrent render still costs a ms of stall). - // The injection step is sync — it's pure string ops on the buffered - // HTML — but the read itself is the only step that touches the disk. - const rawHtml = await readFile(filePath, "utf-8"); - const isIndex = relativePath === "index.html"; - let html = rawHtml; - if (preHeadScripts.length > 0) { - html = injectScriptsAtHeadStart(html, preHeadScripts); + const { fd, stat, filePath } = file; + let streamOwnsFd = false; + try { + const ext = extname(filePath).toLowerCase(); + const contentType = MIME_TYPES[ext] || "application/octet-stream"; + + if (ext === ".html") { + // Use the async read here so we don't block the Node event loop while + // reading an HTML file (typically small, but a 200KB+ AI-generated + // composition during a concurrent render still costs a ms of stall). + // The injection step is sync — it's pure string ops on the buffered + // HTML — but the read itself is the only step that touches the disk. + const rawHtml = await readFileAsync(fd, "utf-8"); + const isIndex = relativePath === "index.html"; + let html = rawHtml; + if (preHeadScripts.length > 0) { + html = injectScriptsAtHeadStart(html, preHeadScripts); + } + html = isIndex + ? injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbeddedRuntime) + : html; + return c.text(html, 200, { "Content-Type": contentType }); } - html = isIndex - ? injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbeddedRuntime) - : html; - return c.text(html, 200, { "Content-Type": contentType }); - } - // Stream binary file content rather than buffering it with readFileSync. - // On video-heavy compositions Chrome requests several 32MB video files - // back-to-back through this server; each readFileSync(32MB) blocked the - // Node event loop long enough to wedge concurrent /health responses (see - // renderOrchestrator.ts:1277-1306 documenting the same regression class). - // createReadStream() pipes bounded chunks asynchronously, so the event - // loop stays responsive even when several large assets are in flight - // simultaneously. Chrome reassembles the chunks transparently. - // - // We also honor `Range:` requests (RFC 7233) so Chrome's