santifer/career-ops · warning

⚠️ Temporary HTML cleanup failed

Error message

⚠️  Temporary HTML cleanup failed: ${err.message}

What it means

Warning logged when deleting the temporary HTML file written for the render fails with any error other than ENOENT. The temp file (tmpHtmlPath) is page.setContent/page.goto input during rendering; a stale temp file is left behind on disk if this fires.

Solutions

  1. Ignore if rare — one stale temp file is harmless; delete it manually
  2. Check permissions/ownership of the temp directory and run with write access
  3. Ensure parallel runs do not share the same temp path
  4. Free disk space if ENOSPC
Defensive patterns

Strategy: try-catch

Validate before calling

import { accessSync, constants, statSync } from 'node:fs';
function canUnlink(p) {
  try { statSync(p); return constants.W_OK; } catch { return false; }
}

Try / catch

await unlink(tmpHtmlPath).catch((err) => {
  if (err?.code !== 'ENOENT') console.warn(`Temp cleanup failed: ${err.message}`);
});

Prevention

When it happens

Trigger: fs unlink(tmpHtmlPath) rejects with a non-ENOENT code in the finally block — filesystem permission issue, temp file on a read-only or full volume, or the file locked/removed by another process (EBUSY/EPERM) while the renderer still held it open.

Common situations: Running with a read-only output/tmp directory (container with restricted mounts); disk quota exceeded; two generate-pdf runs in parallel using the same temp path; Windows file-locking in WSL setups.

Related errors


AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16). Data as JSON: /api/errors/7f7c51c1e6663bc0. Report an issue: GitHub.

Appendix: source

Thrown at generate-pdf.mjs:1812

    // Close the page so a batch does not accumulate pages into the shared
    // browser (leak → OOM). Optional-chained: the single path's browser.close()
    // already reclaims the page, and minimal test doubles may omit close().
    if (page && typeof page.close === 'function') {
      await page.close().catch((err) => {
        console.warn(`⚠️  Page cleanup failed: ${err.message}`);
      });
    }
    // Close the per-document context too, so the JS-disabled context created
    // above does not accumulate in the shared browser across a batch (#2384).
    if (context && typeof context.close === 'function') {
      await context.close().catch((err) => {
        console.warn(`⚠️  Context cleanup failed: ${err.message}`);
      });
    }
    // Clean up temp file
    await unlink(tmpHtmlPath).catch((err) => {
      if (err?.code !== 'ENOENT') {
        console.warn(`⚠️  Temporary HTML cleanup failed: ${err.message}`);
      }
    });
  }
}

/**
 * Render many already-normalized HTML documents through ONE shared Chromium.
 *
 * Maintainer conditions (#2384): the browser is launched once via the same
 * opts.launchBrowser seam the single path uses, and closed in a finally at the
 * batch boundary — it never outlives the batch and is torn down even if a
 * document throws. Each entry renders on its own page (renderInPage), and a
 * failing entry is captured as `{ok:false, error}` without stopping the rest.
 *
 * The browser is owned here: renderBatch closes whatever launchBrowser returns.
 * launchBrowser is a launch *factory*, not a caller-owned handle, so this does
 * not break test injection — the stub returns a fresh browser to be closed.
 *

View on GitHub (pinned to aac998c7ed)