From 7a205eeb1239f6fa0d1e3c84c744325b9e953237 Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Tue, 16 Jun 2026 18:22:08 +0800 Subject: [PATCH] fix(api): isolate the factory validation scratch dir from the worker's The tool factory created its per-request validation scratch dir at tmpdir()/snapotter-scratch/ -- the same path the BullMQ worker uses for job processing (worker.ts scratchRoot()/) whenever SCRATCH_PATH is unset. For sync tools this is harmless because the factory blocks on the worker before its finally cleanup runs. For "long" tools the factory returns 202 immediately and then rm's that dir, racing the worker that is writing the job's input there -- so tools that stage input in the worker scratch dir (epub-convert, to-epub) intermittently failed with ENOENT. Production never hit this because it sets SCRATCH_PATH, diverging the two paths. Give the factory's validation dir a -prep suffix so its cleanup can never touch the worker's job dir. --- apps/api/src/routes/tool-factory.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/api/src/routes/tool-factory.ts b/apps/api/src/routes/tool-factory.ts index c27c4eb1..fa32b1c5 100644 --- a/apps/api/src/routes/tool-factory.ts +++ b/apps/api/src/routes/tool-factory.ts @@ -341,8 +341,14 @@ export function createToolRoute(app: FastifyInstance, config: ToolRouteConfig const toolMeta = TOOLS.find((t) => t.id === config.toolId); const modality = toolMeta?.modality ?? "image"; - // Per-request scratch dir for handlers that need temp files - const scratchDir = join(tmpdir(), "snapotter-scratch", jobId); + // Per-request scratch dir for input handlers that need temp files during + // validation. MUST stay distinct from the worker's job scratch dir + // (worker.ts scratchRoot()/): for "long" tools the factory returns + // 202 below and the `finally` rm's this dir immediately, which would race + // and delete the worker's input mid-job whenever SCRATCH_PATH is unset + // (both otherwise default to tmpdir()/snapotter-scratch/). The + // "-prep" suffix keeps the two from colliding. + const scratchDir = join(tmpdir(), "snapotter-scratch", `${jobId}-prep`); await mkdir(scratchDir, { recursive: true }); try { // Reject files whose extension is not in the tool's acceptedInputs.