Skip to content

Commit 2f32f29

Browse files
jaidhyaniJai
authored andcommitted
fix: tolerate unresponsive tabs when connecting to existing browser
When connecting to a running browser via --browserUrl, browser.pages() hangs indefinitely if any tab is unresponsive (discarded by memory management, dead localhost, etc). This blocks all tool calls since page enumeration runs on every request. Two changes: 1. Add protocolTimeout (15s) to puppeteer.connect() so CDP commands fail with an error instead of hanging forever. 2. Wrap browser.pages() in #getAllPages() with a 10s timeout. If it fails, fall back to per-target enumeration: iterate browser.targets(), call target.page() on each with a 5s timeout, and skip tabs that don't respond. This way one unresponsive tab doesn't block the entire server. Tested with Chrome/Brave 145, 28 open tabs (5 discarded). Without this fix the server hangs forever on the first tool call. With the fix it returns 23 responsive pages in ~12s and logs the 5 it skipped. Related: puppeteer/puppeteer#5633, puppeteer/puppeteer#14708, #870, #978
1 parent 9236834 commit 2f32f29

2 files changed

Lines changed: 42 additions & 3 deletions

File tree

src/McpContext.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -562,9 +562,47 @@ export class McpContext implements Context {
562562
isolatedContextNames: Map<Page, string>;
563563
}> {
564564
const defaultCtx = this.browser.defaultBrowserContext();
565-
const allPages = await this.browser.pages(
566-
this.#options.experimentalIncludeAllPages,
567-
);
565+
566+
// Tolerant page enumeration: browser.pages() hangs indefinitely when
567+
// any tab is unresponsive (e.g. discarded/sleeping tabs). Fall back to
568+
// per-target enumeration so that one bad tab doesn't block the server.
569+
let allPages: Page[];
570+
try {
571+
allPages = await Promise.race([
572+
this.browser.pages(this.#options.experimentalIncludeAllPages),
573+
new Promise<never>((_, reject) =>
574+
setTimeout(
575+
() => reject(new Error('browser.pages() timed out')),
576+
10_000,
577+
),
578+
),
579+
]);
580+
} catch {
581+
this.logger(
582+
'browser.pages() timed out — falling back to per-target enumeration',
583+
);
584+
const pageTargets = this.browser
585+
.targets()
586+
.filter(t => t.type() === 'page');
587+
allPages = [];
588+
await Promise.all(
589+
pageTargets.map(async target => {
590+
try {
591+
const page = await Promise.race([
592+
target.page(),
593+
new Promise<never>((_, reject) =>
594+
setTimeout(() => reject(new Error('timeout')), 5_000),
595+
),
596+
]);
597+
if (page) {
598+
allPages.push(page);
599+
}
600+
} catch {
601+
this.logger(`Skipping unresponsive tab: ${target.url()}`);
602+
}
603+
}),
604+
);
605+
}
568606

569607
const allTargets = this.browser.targets();
570608
const extensionTargets = allTargets.filter(target => {

src/browser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export async function ensureBrowserConnected(options: {
6161
targetFilter: makeTargetFilter(enableExtensions),
6262
defaultViewport: null,
6363
handleDevToolsAsPage: true,
64+
protocolTimeout: 15_000,
6465
};
6566

6667
let autoConnect = false;

0 commit comments

Comments
 (0)