{"record":{"id":"7f7c51c1e6663bc0","repo":"santifer/career-ops","slug":"temporary-html-cleanup-failed-err-message","errorCode":null,"errorMessage":"⚠️  Temporary HTML cleanup failed: ${err.message}","messagePattern":"⚠️  Temporary HTML cleanup failed: (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"generate-pdf.mjs","lineNumber":1597,"sourceCode":"    // Close the page so a batch does not accumulate pages into the shared\n    // browser (leak → OOM). Optional-chained: the single path's browser.close()\n    // already reclaims the page, and minimal test doubles may omit close().\n    if (page && typeof page.close === 'function') {\n      await page.close().catch((err) => {\n        console.warn(`⚠️  Page cleanup failed: ${err.message}`);\n      });\n    }\n    // Close the per-document context too, so the JS-disabled context created\n    // above does not accumulate in the shared browser across a batch (#2384).\n    if (context && typeof context.close === 'function') {\n      await context.close().catch((err) => {\n        console.warn(`⚠️  Context cleanup failed: ${err.message}`);\n      });\n    }\n    // Clean up temp file\n    await unlink(tmpHtmlPath).catch((err) => {\n      if (err?.code !== 'ENOENT') {\n        console.warn(`⚠️  Temporary HTML cleanup failed: ${err.message}`);\n      }\n    });\n  }\n}\n\n/**\n * Render many already-normalized HTML documents through ONE shared Chromium.\n *\n * Maintainer conditions (#2384): the browser is launched once via the same\n * opts.launchBrowser seam the single path uses, and closed in a finally at the\n * batch boundary — it never outlives the batch and is torn down even if a\n * document throws. Each entry renders on its own page (renderInPage), and a\n * failing entry is captured as `{ok:false, error}` without stopping the rest.\n *\n * The browser is owned here: renderBatch closes whatever launchBrowser returns.\n * launchBrowser is a launch *factory*, not a caller-owned handle, so this does\n * not break test injection — the stub returns a fresh browser to be closed.\n *","sourceCodeStart":1579,"sourceCodeEnd":1615,"githubUrl":"https://github.com/santifer/career-ops/blob/60398d6549a46f5266929538af21cfab94badc75/generate-pdf.mjs#L1579-L1615","documentation":"fs.promises.unlink on the temporary HTML file failed with an error code other than ENOENT (ENOENT is deliberately ignored because the temp file may already be gone). The rendered PDF is unaffected — only the intermediate .html file next to it survives on disk. Typical codes: EPERM/EBUSY when another process (Chromium, antivirus, indexer) still holds the handle — classic on Windows — or EACCES/EROFS when the output directory's permissions or mount changed between write and cleanup.","triggerScenarios":"Windows: Chromium or real-time antivirus still holds the temp HTML open when unlink runs (EBUSY/EPERM); the output dir was chmod'ed or is on a read-only mount (EACCES/EROFS); tmpHtmlPath points at a directory (EISDIR/EPERM); a network filesystem with lazy handle release.","commonSituations":"Windows dev machines with antivirus scanning generated files; CI containers writing into a root-owned output dir; NFS/SMB mounts with deferred handle release.","solutions":["Ignore it — delete the leftover .html manually; the PDF is complete.","On Windows, retry the delete after a short delay, once Chromium has released the handle.","If EACCES/EPERM repeats, fix ownership/permissions of the output directory (chmod/chown).","Exclude the output directory from antivirus scanning."],"exampleFix":"// before\nawait unlink(tmpHtmlPath).catch((err) => {\n  if (err?.code !== 'ENOENT') {\n    console.warn(`⚠️  Temporary HTML cleanup failed: ${err.message}`);\n  }\n});\n// after — one retry for transient handle locks (EBUSY/EPERM, common on Windows)\nawait unlink(tmpHtmlPath).catch(async (err) => {\n  if (err?.code === 'EBUSY' || err?.code === 'EPERM') {\n    await new Promise((r) => setTimeout(r, 250));\n    return unlink(tmpHtmlPath).catch((err2) => {\n      if (err2?.code !== 'ENOENT') console.warn(`⚠️  Temporary HTML cleanup failed: ${err2.message}`);\n    });\n  }\n  if (err?.code !== 'ENOENT') console.warn(`⚠️  Temporary HTML cleanup failed: ${err.message}`);\n});","handlingStrategy":"retry","validationCode":"import { stat } from 'node:fs/promises';\nconst st = await stat(tmpHtmlPath).catch(() => null);\nif (st?.isFile()) await unlink(tmpHtmlPath); // only attempt the delete when it exists and is a file","typeGuard":null,"tryCatchPattern":"try {\n  await unlink(tmpHtmlPath);\n} catch (err) {\n  if (err.code === 'EBUSY' || err.code === 'EPERM') await sleep(250).then(() => unlink(tmpHtmlPath)).catch(() => {});\n  else if (err.code !== 'ENOENT') console.warn(err.message);\n} // classify by err.code: ENOENT=ignore, EBUSY/EPERM=retry once, rest=warn","preventionTips":["Close or release anything that read the temp HTML before unlinking it","Write temp files to os.tmpdir() instead of next to the PDF when the output dir may be locked","Exclude build output dirs from antivirus and indexer scanning on Windows"],"tags":["fs","unlink","temp-files","windows-file-lock","permissions"],"backgroundTag":"fs-unlink-eperm","analyzedSha":"60398d6549a46f5266929538af21cfab94badc75","analyzedAt":"2026-08-20T23:00:06.764Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}