santifer/career-ops · warning

⚠️ Page cleanup failed: ${err.message}

Error message

⚠️  Page cleanup failed: ${err.message}

What it means

Best-effort cleanup warning in generate-pdf.mjs's per-document finally: after the PDF result is returned (or an error propagates), page.close() rejected and is logged but never rethrown, so cleanup noise cannot overwrite a real result. The guard is defensive because the single-CV path's browser.close() already reclaims the page and minimal test doubles may omit close().

Source

Thrown at generate-pdf.mjs:1584

    console.log(`📊 Pages: ${pageCount}`);
    console.log(`📦 Size: ${(pdfBuffer.length / 1024).toFixed(1)} KB`);

    try {
      updatePDFManifest(reportNum, outputPath, inputPath, format);
      console.log(`🔗 Manifest: data/pdf-index.tsv updated${reportNum ? ` (report ${reportNum})` : ' (no --report given)'}`);
    } catch (err) {
      // The PDF itself succeeded — never fail the run over manifest bookkeeping.
      console.error(`⚠️  Manifest update failed: ${err.message}`);
    }

    return { outputPath, pageCount, size: pdfBuffer.length };
  } finally {
    // 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}`);
      }
    });
  }
}

View on GitHub (pinned to 60398d6549)

Solutions

  1. Ignore it if the render returned — the returned {outputPath, pageCount, size} is authoritative
  2. For batch instability, lower batch size / disable JS injection if pages keep crashing, and watch memory
  3. In tests, make page/context doubles' close() resolve instead of reject
  4. Keep one owner per lifecycle layer: pages/contexts close in renderInPage's finally; the browser closes in renderHtmlToPdf's finally
Defensive patterns

Strategy: fallback

Validate before calling

// For test doubles: make close() resolve so the best-effort path stays silent
function makeFakePage() {
  return { close: async () => {}, goto: async () => {}, pdf: async () => Buffer.from('') };
}

Type guard

function isCloseablePage(p) {
  return p != null && typeof p.close === 'function';
}

Try / catch

// Pattern already applied by renderInPage's finally; mirror it if you manage pages yourself:
if (page && typeof page.close === 'function') {
  await page.close().catch((err) => console.warn(`page close failed (ignored): ${err.message}`));
}

Prevention

When it happens

Trigger: Batch rendering (renderInPage on a shared browser) where the page or its Chromium process already died (tab crash, OOM, outer browser.close() racing the per-page close), or a mocked page whose close() rejects. Triggered only when page exists and exposes a close function.

Common situations: Long batch runs accumulating crashed pages before the context/browser close; test doubles returning a page stub whose close() throws; interleaved browser-level and page-level cleanup in custom harnesses.

Related errors


AI-assisted analysis of santifer/career-ops@60398d6549 (2026-08-20). Data as JSON: /api/errors/b86842692f4e5085. Report an issue: GitHub.