Skip to content

inject() fails after sleep/resume due to execution context destruction during page navigation #127082

@Adi1231234

Description

@Adi1231234

Describe the bug.

After a system sleep/resume cycle, Chrome may perform an internal page navigation to recover corrupted IndexedDB storage. This navigation destroys the current execution context.

In inject(), the polling loops that wait for window.Debug?.VERSION and window.Store use page.evaluate() repeatedly. When the execution context is destroyed mid-poll, page.evaluate() throws "Execution context was destroyed", causing inject() to fail.

Additionally, the framenavigated listener (which calls inject() again after navigation) is registered after the initial inject() call in initialize(). This means if inject() fails due to navigation, the recovery mechanism is not yet in place.

Expected Behavior

inject() should survive internal page navigations (such as IndexedDB recovery) and continue polling in the new execution context without throwing.

Steps to Reproduce the Bug or Issue

  1. Connect a WhatsApp session using LocalAuth
  2. Put the system to sleep for a few minutes
  3. Resume the system
  4. Chrome performs an internal navigation to recover IndexedDB
  5. inject() throws "Execution context was destroyed" or "auth timeout"
  6. The client disconnects and may show a QR code on the next attempt

The root cause can also be reproduced programmatically:

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({ headless: 'new' });
    const page = await browser.newPage();
    await page.setContent('<html><body></body></html>');

    // Start a long-running evaluate (simulates polling mid-navigation)
    const evalPromise = page.evaluate(async () => {
        await new Promise(r => setTimeout(r, 5000));
        return 'done';
    }).catch(err => console.log('evaluate threw:', err.message));

    // Navigate while evaluate is running (simulates IndexedDB recovery)
    await new Promise(r => setTimeout(r, 200));
    await page.goto('data:text/html,<html><body>New Page</body></html>');

    await evalPromise;
    // Output: evaluate threw: Execution context was destroyed.

    // In contrast, waitForFunction survives navigation:
    await page.setContent('<html><body></body></html>');
    const waitPromise = page.waitForFunction(
        'window.testReady === true',
        { timeout: 10000 }
    );
    await new Promise(r => setTimeout(r, 200));
    await page.goto('data:text/html,<html><body><script>window.testReady = true;</script></body></html>');
    await waitPromise;
    console.log('waitForFunction survived navigation!');

    await browser.close();
})();

WhatsApp Account Type

Standard

Browser Type

Chromium (bundled with Puppeteer)

Operation System Type

Tested on Windows 11 and Linux (Ubuntu)

Phone OS Type

Android

WhatsApp-Web.js Version

1.26.1-alpha.3

WhatsApp Web Version

2.3000+

Node.js Version

v18+

Authentication Strategy

LocalAuth

Additional Context

Root cause analysis:

page.evaluate() is bound to a single execution context. When Chrome navigates internally (e.g., for IndexedDB recovery), that context is destroyed and evaluate() throws.

page.waitForFunction() is designed to survive navigation. Internally, Puppeteer's WaitTask treats "Execution context was destroyed" as a non-fatal error (returns undefined from getBadError()), and when a new context is created, IsolatedWorld calls taskManager.rerunAll() to re-evaluate all waiting tasks in the new context.

Fix (3 changes in src/Client.js):

  1. Replace the manual page.evaluate() polling loop for Debug.VERSION with page.waitForFunction()
  2. Replace the manual page.evaluate() polling loop for Store with page.waitForFunction()
  3. Move the framenavigated listener registration to before inject() in initialize(), so the recovery mechanism is in place from the start

This fix uses no retry logic, no try/catch wrapping, and no error swallowing. It simply uses the correct Puppeteer API that natively handles execution context lifecycle.

Verified with Puppeteer 18.x (used by wwjs main) and 24.x - both versions have the same WaitTask resilience mechanism.

Included E2E tests that deterministically prove:

  • page.evaluate() throws during navigation (the bug)
  • page.waitForFunction() survives navigation (the fix)
  • The full fix (both mechanisms together) works correctly

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions