Skip to content

Commit 29b9412

Browse files
committed
fix: skip frozen/discarded targets in page enumeration
1 parent 6ac30dc commit 29b9412

2 files changed

Lines changed: 71 additions & 4 deletions

File tree

src/McpContext.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -564,11 +564,47 @@ export class McpContext implements Context {
564564
isolatedContextNames: Map<Page, string>;
565565
}> {
566566
const defaultCtx = this.browser.defaultBrowserContext();
567-
const allPages = await this.browser.pages(
568-
this.#options.experimentalIncludeAllPages,
569-
);
570567

568+
// Enumerate targets individually instead of calling browser.pages() so
569+
// that a single frozen/discarded background tab that times out on
570+
// Network.enable cannot abort the entire page enumeration (see #1230).
571571
const allTargets = this.browser.targets();
572+
const pageTargets = allTargets.filter(target => {
573+
const type = target.type();
574+
if (type === 'page') return true;
575+
if (this.#options.experimentalIncludeAllPages) {
576+
return type === 'background_page' || type === 'webview';
577+
}
578+
return false;
579+
});
580+
const pageResults = await Promise.all(
581+
pageTargets.map(async target => {
582+
try {
583+
const page = await Promise.race([
584+
target.page(),
585+
new Promise<null>(resolve =>
586+
setTimeout(() => resolve(null), DEFAULT_TIMEOUT),
587+
),
588+
]);
589+
if (!page) {
590+
this.logger(
591+
'Timed out attaching to target at',
592+
target.url(),
593+
'— likely frozen or discarded',
594+
);
595+
}
596+
return page;
597+
} catch (err) {
598+
this.logger(
599+
'Skipping frozen/discarded target at',
600+
target.url(),
601+
err,
602+
);
603+
return null;
604+
}
605+
}),
606+
);
607+
const allPages = pageResults.filter((p): p is Page => p !== null);
572608
const extensionTargets = allTargets.filter(target => {
573609
return (
574610
target.url().startsWith('chrome-extension://') &&
@@ -578,7 +614,12 @@ export class McpContext implements Context {
578614

579615
for (const target of extensionTargets) {
580616
// Right now target.page() returns null for popup and side panel pages.
581-
let page = await target.page();
617+
let page = await Promise.race([
618+
target.page(),
619+
new Promise<null>(resolve =>
620+
setTimeout(() => resolve(null), DEFAULT_TIMEOUT),
621+
),
622+
]);
582623
if (!page) {
583624
// We need to cache pages instances for targets because target.asPage()
584625
// returns a new page instance every time.

tests/McpContext.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import path from 'node:path';
99
import {afterEach, describe, it} from 'node:test';
1010
import {pathToFileURL} from 'node:url';
1111

12+
import type {Target} from 'puppeteer-core';
1213
import sinon from 'sinon';
1314

1415
import {NetworkFormatter} from '../src/formatters/NetworkFormatter.js';
@@ -257,4 +258,29 @@ describe('McpContext', () => {
257258
);
258259
});
259260
});
261+
262+
it('should skip frozen targets that hang on target.page()', async () => {
263+
await withMcpContext(async (_response, context) => {
264+
const browser = context.browser;
265+
const realTargets = browser.targets();
266+
267+
// Inject a fake target whose page() never resolves (simulates a frozen tab).
268+
const frozenTarget = {
269+
type: () => 'page',
270+
url: () => 'https://frozen-tab.example.com',
271+
page: () => new Promise<null>(() => {}),
272+
} as unknown as Target;
273+
274+
sinon.stub(browser, 'targets').returns([...realTargets, frozenTarget]);
275+
276+
const start = Date.now();
277+
const pages = await context.createPagesSnapshot();
278+
const elapsed = Date.now() - start;
279+
280+
// The frozen target should be skipped, not block for 180s.
281+
assert.ok(elapsed < 15_000, `Took ${elapsed}ms, expected < 15s`);
282+
// Real pages should still be enumerated.
283+
assert.ok(pages.length > 0, 'Should still enumerate healthy pages');
284+
});
285+
});
260286
});

0 commit comments

Comments
 (0)