Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/browser/page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,28 @@ describe('Page active target tracking', () => {
expect(retryCall[1]).not.toHaveProperty('page');
});

it('retries on a bare "Page not found:" error without the stale-identity suffix', async () => {
// Under concurrent adapter calls the extension can reject with just
// "Page not found: <id>" (no "— stale page identity" suffix) when the cached
// targetId was evicted. That still means the identity is dead, so goto() must
// drop it and retry once instead of cascading failures.
sendCommandFullMock
.mockResolvedValueOnce({ data: { url: 'https://example.com/first' }, page: 'page-1' })
.mockRejectedValueOnce(new Error('Page not found: deadbeef'))
.mockResolvedValueOnce({ data: { url: 'https://example.com/second' }, page: 'page-2' });

const page = new Page('site:youtube', undefined, undefined, undefined, 'adapter', 'persistent');

await page.goto('https://example.com/first', { waitUntil: 'none' });
await page.goto('https://example.com/second', { waitUntil: 'none' });
expect(page.getActivePage()).toBe('page-2');

expect(sendCommandFullMock).toHaveBeenCalledTimes(3);
const retryCall = sendCommandFullMock.mock.calls[2];
expect(retryCall[0]).toBe('navigate');
expect(retryCall[1]).not.toHaveProperty('page');
});

it('does not retry stale page errors when no identity was cached', async () => {
// _page is undefined on a fresh Page — there's nothing to drop, so propagate the
// error instead of silently retrying with the same params.
Expand Down
8 changes: 4 additions & 4 deletions src/browser/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function isUnsupportedNetworkCaptureError(err: unknown): boolean {
// to the session lease (or create a fresh tab).
function isStalePageIdentityError(err: unknown): boolean {
const message = err instanceof Error ? err.message : String(err);
return message.includes('stale page identity');
return message.includes('stale page identity') || message.includes('Page not found:');
}

/**
Expand Down Expand Up @@ -94,9 +94,9 @@ export class Page extends BasePage {
} catch (err) {
// If our cached targetId went stale (tab closed externally, identity evicted),
// drop the dead id and retry without it — the extension will resolve through the
// session lease or open a fresh automation tab. Without this, subsequent
// navigations in the same Page instance keep re-sending the same dead targetId
// and cascade into "Page not found:" failures.
// session lease or open a fresh automation tab. Without this, every subsequent
// adapter call in the same process keeps re-sending the same dead targetId and
// cascades into "Page not found:" failures across concurrent calls.
if (!isStalePageIdentityError(err) || this._page === undefined) throw err;
this._page = undefined;
result = await sendCommandFull('navigate', {
Expand Down
Loading