santifer/career-ops · warning

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

Error message

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

What it means

Playwright's BrowserContext.close() rejected during the finally-block cleanup of the single-document PDF render path in generate-pdf.mjs. This close exists so the per-document JS-disabled context does not accumulate in the shared browser across a batch (#2384); when it fails, the context's resources are only reclaimed later, when the shared browser itself closes. Usual causes: the Chromium process already died (crash or OOM kill), an earlier failure path already tore the context down (double close), or the Playwright driver lost its connection.

Source

Thrown at generate-pdf.mjs:1591

      // 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}`);
      }
    });
  }
}

/**
 * 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

View on GitHub (pinned to 60398d6549)

Solutions

  1. Treat as non-fatal: verify the PDF itself was written — this warning is cleanup-only.
  2. If it recurs in batches, render fewer documents per browser or raise the memory ceiling; in shared containers add launch args like --disable-dev-shm-usage.
  3. Run npx playwright install chromium so the driver and the browser build match.
  4. Check dmesg or CI logs for an OOM kill of chromium before assuming a Playwright bug.

Example fix

// before
if (context && typeof context.close === 'function') {
  await context.close().catch((err) => {
    console.warn(`⚠️  Context cleanup failed: ${err.message}`);
  });
}
// after — skip the doomed close when the shared browser is already gone
if (context && typeof context.close === 'function' && (browser ? browser.isConnected() : true)) {
  await context.close().catch((err) => {
    console.warn(`⚠️  Context cleanup failed: ${err.message}`);
  });
}
Defensive patterns

Strategy: try-catch

Validate before calling

const canCloseContext = (browser, context) =>
  Boolean(context && typeof context.close === 'function') &&
  (browser ? browser.isConnected() : true);
if (canCloseContext(browser, context)) {
  await context.close();
}

Type guard

const isCloseableContext = (c, browser) =>
  Boolean(c && typeof c.close === 'function') && (browser ? browser.isConnected() : true);

Try / catch

await context.close().catch((err) => {
  // warn-and-continue: cleanup must never mask the render result, and a
  // browser that already died cannot be re-closed — expect /closed/i messages.
  console.warn(`Context cleanup failed: ${err.message}`);
});

Prevention

When it happens

Trigger: Chromium crashes or is OOM-killed mid-render, so context.close() rejects with 'Target closed' / 'Browser has been closed'; a prior error path already closed the context; the playwright npm package and the installed browser build are version-mismatched; a test double's close() rejects.

Common situations: Long CV batch renders under memory pressure (CI runners, containers with a small /dev/shm); upgrading playwright without running npx playwright install; minimal mocks in tests that throw on close.

Related errors


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