Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/bitter-years-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/stagehand": patch
---

Fix unhandled CDP detaches by returning the original sendCDP promise
55 changes: 55 additions & 0 deletions packages/core/lib/v3/tests/cdp-session-detached.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { test, expect } from "@playwright/test";
import { chromium as playwrightChromium } from "playwright";
import { V3 } from "../v3";
import { v3TestConfig } from "./v3.config";

test.describe("CDP session detach handling", () => {
let v3: V3;

test.beforeEach(async () => {
v3 = new V3(v3TestConfig);
await v3.init();
});

test.afterEach(async () => {
await v3?.close?.().catch(() => {});
});

test("rejects inflight CDP calls when a target is closed", async () => {
const unhandled: unknown[] = [];
const onUnhandled = (reason: unknown) => {
unhandled.push(reason);
};

process.on("unhandledRejection", onUnhandled);

let pwBrowser: Awaited<
ReturnType<typeof playwrightChromium.connectOverCDP>
> | null = null;

try {
pwBrowser = await playwrightChromium.connectOverCDP(v3.connectURL());
const pwContext = pwBrowser.contexts()[0];
const pwPage = pwContext.pages()[0];

const v3Page = v3.context.pages()[0];
await v3Page.goto("data:text/html,<html><body>cdp</body></html>");

const pending = v3Page.sendCDP("Runtime.evaluate", {
expression: "new Promise(r => setTimeout(() => r('done'), 5000))",
awaitPromise: true,
returnByValue: true,
});

await pwPage.close();

await expect(pending).rejects.toThrow(/CDP session detached/);

await new Promise((r) => setTimeout(r, 50));
expect(unhandled).toHaveLength(0);
} finally {
process.off("unhandledRejection", onUnhandled);
await pwBrowser?.close().catch(() => {});
}
});
});
4 changes: 4 additions & 0 deletions packages/core/lib/v3/understudy/cdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ export class CdpConnection implements CDPSessionLike {
ts: Date.now(),
});
});
// Prevent unhandledRejection if a session detaches before the caller awaits.
void p.catch(() => {});
this.cdpLogger?.({ method, params, targetId: null });
this.ws.send(JSON.stringify(payload));
return p;
Expand Down Expand Up @@ -266,6 +268,8 @@ export class CdpConnection implements CDPSessionLike {
ts: Date.now(),
});
});
// Prevent unhandledRejection if a session detaches before the caller awaits.
void p.catch(() => {});
const targetId = this.sessionToTarget.get(sessionId) ?? null;
this.cdpLogger?.({ method, params, targetId });
this.ws.send(JSON.stringify(payload));
Expand Down
5 changes: 1 addition & 4 deletions packages/core/lib/v3/understudy/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,10 +607,7 @@ export class Page {
* { expression: "1 + 1" }
* );
*/
public async sendCDP<T = unknown>(
method: string,
params?: object,
): Promise<T> {
public sendCDP<T = unknown>(method: string, params?: object): Promise<T> {
return this.mainSession.send<T>(method, params);
}

Expand Down
Loading