santifer/career-ops · warning
⚠️ Browser cleanup failed
Error message
⚠️ Browser cleanup failed: ${err.message} What it means
A non-fatal warning emitted when browser.close() on the Playwright browser instance rejects during the finally block of a single-document PDF render. The PDF may already have been produced; only teardown failed, typically because the browser process already died or the connection to it was lost.
Solutions
- Ignore if the PDF was written — this is teardown-only; verify the output file exists
- Update Playwright (npx playwright install --with-deps) so the browser build matches the driver and stops crashing
- Check memory limits (dmesg / container OOM logs) and reduce batch concurrency
- If it recurs with no crash, wrap close with a browser.process kill fallback
Example fix
// before
await browser.close().catch((err) => console.warn(`⚠️ Browser cleanup failed: ${err.message}`));
// after
await browser.close().catch((err) => {
if (!err.message.includes('Target closed') && !err.message.includes('Connection closed')) {
console.warn(`⚠️ Browser cleanup failed: ${err.message}`);
}
browser.process()?.kill('SIGKILL');
}); Defensive patterns
Strategy: try-catch
Type guard
function isBenignTeardownError(err) {
const msg = String(err?.message ?? '');
return /Target closed|Connection closed|Browser has been closed|Target crashed/i.test(msg);
} Try / catch
await browser.close().catch((err) => {
if (!isBenignTeardownError(err)) console.warn(`Browser cleanup failed: ${err.message}`);
}); Prevention
- Keep Playwright and its browser builds in sync (npx playwright install)
- Give headless Chromium adequate memory in batch environments
- Install OS dependencies (playwright install-deps) before running in CI/containers
- Check the rendered output file exists before treating teardown warnings as failures
When it happens
Trigger: browser.close() rejects inside the finally of the render helper after launchBrowser({headless:true}) — Playwright browser crashed mid-render, connection closed because the Chromium process was killed (OOM, signal), or the target closed unexpectedly before cleanup.
Common situations: Chromium OOM-killed on large HTML during a long batch; headless Chromium crash from a missing system dependency; container sandbox killing zombie browser processes; calling browser.close() twice via overlapping code paths in a customized fork.
Related errors
- archive render skipped — no Playwright browser in env
- ⚠️ Context cleanup failed
- [fetch] Playwright error
- [fetch] Playwright unavailable — falling back to plain…
- --liveness requires Playwright with Chromium (run "npx…
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/208d8ea19efd2630.
Report an issue: GitHub.
Appendix: source
Thrown at generate-pdf.mjs:1646
* reportNum?: string,
* inputPath?: string,
* workspaceRoot?: string,
* maxPages?: number,
* strictPages?: boolean,
* launchBrowser?: (options: {headless: boolean}) => Promise<import('playwright').Browser>
* }} [opts]
* @returns {Promise<{outputPath: string, pageCount: number, size: number}>}
*/
export async function renderHtmlToPdf(html, outputPath, opts = {}) {
const launchBrowser = opts.launchBrowser || ((options) => chromium.launch(options));
let browser = null;
try {
browser = await launchBrowser({ headless: true });
return await renderInPage(browser, html, outputPath, opts);
} finally {
if (browser) {
await browser.close().catch((err) => {
console.warn(`⚠️ Browser cleanup failed: ${err.message}`);
});
}
}
}
/**
* Render one already-normalized HTML document to a PDF on an already-launched
* browser. This is the page-level half of the render — it owns the per-document
* work (theme/print/font injection, temp file, page, PDF, page-budget, manifest)
* but NOT the browser lifecycle. Both the single-CV path (renderHtmlToPdf) and
* the batch path (renderBatch) call this exact function, which is what keeps a
* single-CV render byte-identical whether it runs alone or inside a batch (#2384).
*
* The page and the temp HTML file are always cleaned up in a finally, so a
* throw here (e.g. a strict page-budget overflow) never leaks a page into the
* shared browser — the caller's remaining documents keep their own fresh pages.
*
* @param {import('playwright').Browser} browser - An open browser to render on.View on GitHub (pinned to aac998c7ed)