Skip to content

[Detail Bug] CLI Lighthouse runs can leak browser contexts when audits fail #693

@detail-app

Description

@detail-app

Detail Bug Report

https://app.detail.dev/org_06887db3-bf54-40ab-976d-46c66ab2b840/bugs/bug_5cd063b8-bda8-4e41-99b0-5d895835fb30

Summary

  • Context: The lighthouse package provides a wrapper function that manages browser context lifecycle and runs lighthouse audits on web pages.
  • Bug: When browserless.withPage() throws an error (e.g., timeout, navigation failure), the registered teardown callback is never executed.
  • Actual vs. expected: The teardown callback should always execute to clean up the browser context, but it only executes when withPage() succeeds.
  • Impact: Browser contexts are leaked when errors occur during lighthouse execution, leading to resource exhaustion in long-running CLI processes.

Code with Bug

module.exports =
  getBrowserless =>
    async (url, { output = 'json', timeout, flags, ...opts } = {}) => {
      let teardown
      const browserless = await getBrowserless(fn => (teardown = fn))

      const fn = page => async () =>
        lighthouse({
          flags,
          config: await getConfig({ ...opts, output }),
          page,
          url
        })

      const result = await browserless.withPage(fn, { timeout })()
      if (teardown) await teardown()  // <-- BUG 🔴 Never called if withPage() throws, leaking context
      return result
    }

Explanation

teardown() is invoked only after awaiting browserless.withPage(...)(). If withPage() throws (timeouts/navigation failures are expected), control flow skips the teardown call, so browserless.destroyContext() (registered via the teardown callback) never runs.

Codebase Inconsistency

The CLI expects this teardown mechanism to perform cleanup:

const lighthouse = createLighthouse(async teardown => {
  teardown(() => browserless.destroyContext())
  return browserless
})

const report = await lighthouse(url)  // <-- If this throws, destroyContext() never called

Additionally, tests can miss the leak because the test helper registers its own cleanup via the test framework (t.teardown(browserless.destroyContext)), so production behavior differs.

Recommended Fix

Wrap the withPage call in try-finally so teardown always runs:

try {
  const result = await browserless.withPage(fn, { timeout })()
  return result
} finally {
  if (teardown) await teardown()
}

History

This bug was introduced in commit 75c31e5. The commit upgraded Lighthouse from v9.6.8 to v10.0.0 and refactored the lighthouse integration to use a new withPage pattern with a teardown callback mechanism for browser context cleanup. The bug slipped in because the teardown call was placed after the withPage() call without wrapping it in a try-finally block, so the teardown never executes when withPage() throws an error (such as timeout errors).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions