Skip to content

Commit c8dfac0

Browse files
Don't rely on module-level state for Playwright selector engine registration (#1022)
# why This is to fix some undesired behavior for a common dev workflow with `next dev`. Introduced in #954. There is now module-level state in `lib/StagehandPage.ts` (the `stagehandSelectorRegistered` boolean) used to guard against multiple calls to `selectors.register` (a Playwright function which sets module-level state). This used in the function `ensureStagehandSelectorEngine`. This guard exists because calling `selectors.register` with the same string more than once will cause an error. The problem is that `next dev` repeatedly reloads the `stagehand` module whenever we first start up our dev server or make changes, but without always reloading the underlying `playwright` module. <details> <summary>So, we get lots of errors like this.</summary> ```zsh [2025-08-22 12:58:10] web:dev: Error in Inngest task { [2025-08-22 12:58:10] web:dev: error: { [2025-08-22 12:58:10] web:dev: error: 'NonRetriableError', [2025-08-22 12:58:10] web:dev: message: "Hey! We're sorry you ran into an error. \n" + [2025-08-22 12:58:10] web:dev: 'Stagehand version: 2.4.3 \n' + [2025-08-22 12:58:10] web:dev: 'If you need help, please open a Github issue or reach out to us on Slack: https://stagehand.dev/slack\n' + [2025-08-22 12:58:10] web:dev: '\n' + [2025-08-22 12:58:10] web:dev: 'Full error:\n' + [2025-08-22 12:58:10] web:dev: 'selectors.register: "stagehand" selector engine has been already registered', [2025-08-22 12:58:10] web:dev: name: 'Error', [2025-08-22 12:58:10] web:dev: stack: 'StagehandDefaultError: \n' + [2025-08-22 12:58:10] web:dev: "Hey! We're sorry you ran into an error. \n" + [2025-08-22 12:58:10] web:dev: 'Stagehand version: 2.4.3 \n' + [2025-08-22 12:58:10] web:dev: 'If you need help, please open a Github issue or reach out to us on Slack: https://stagehand.dev/slack\n' + [2025-08-22 12:58:10] web:dev: '\n' + [2025-08-22 12:58:10] web:dev: 'Full error:\n' + [2025-08-22 12:58:10] web:dev: 'selectors.register: "stagehand" selector engine has been already registered\n' + [2025-08-22 12:58:10] web:dev: ' at _StagehandPage.eval (webpack-internal:///(rsc)/../../node_modules/.pnpm/@[email protected][email protected][email protected][email protected][email protected]/node_modules/@browserbasehq/stagehand/dist/index.js:4077:15)\n' + [2025-08-22 12:58:10] web:dev: ' at Generator.throw (<anonymous>)\n' + [2025-08-22 12:58:10] web:dev: ' at rejected (webpack-internal:///(rsc)/../../node_modules/.pnpm/@[email protected][email protected][email protected][email protected][email protected]/node_modules/@browserbasehq/stagehand/dist/index.js:73:29)' [2025-08-22 12:58:10] web:dev: }, ``` </details> **TL;DR The `stagehand` module state guard, to guard the Playwright module state, becomes out of sync with Playwright.** This is not really `stagehand`'s "fault". It appears to be `next`-specific behavior combined with some logic to get around funky module-level `playwright` state. But it is causing a lot of friction on our team; I think module-level state is risky in general for this reason. # what changed My proposed fix is to wrap this `selectors.`register call in a specific `try`/`catch` that looks for, and ignores, the specific error `/selector engine has been already registered/` in `packages/playwright-core/src/client/selectors.ts` instead of using the `stagehandSelectorRegistered` boolean. # test plan Existing evals. And this works locally as expected when I build our system against this version, but without the error, no matter how many times the module is reloaded.
1 parent 5d668b8 commit c8dfac0

File tree

2 files changed

+20
-8
lines changed

2 files changed

+20
-8
lines changed

.changeset/neat-walls-walk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand-lib": patch
3+
---
4+
5+
Fixed small issue with module-level state guard for the Playwright selectors.register call

lib/StagehandPage.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,6 @@ async function getCurrentRootFrameId(session: CDPSession): Promise<string> {
3838
return frameTree.frame.id;
3939
}
4040

41-
/** ensure we register the custom selector only once per process */
42-
let stagehandSelectorRegistered = false;
43-
4441
export class StagehandPage {
4542
private stagehand: Stagehand;
4643
private rawPage: PlaywrightPage;
@@ -197,10 +194,7 @@ ${scriptContent} \
197194

198195
/** Register the custom selector engine that pierces open/closed shadow roots. */
199196
private async ensureStagehandSelectorEngine(): Promise<void> {
200-
if (stagehandSelectorRegistered) return;
201-
stagehandSelectorRegistered = true;
202-
203-
await selectors.register("stagehand", () => {
197+
const registerFn = () => {
204198
type Backdoor = {
205199
getClosedRoot?: (host: Element) => ShadowRoot | undefined;
206200
};
@@ -299,7 +293,20 @@ ${scriptContent} \
299293
return out;
300294
},
301295
};
302-
});
296+
};
297+
298+
try {
299+
await selectors.register("stagehand", registerFn);
300+
} catch (err) {
301+
if (
302+
err instanceof Error &&
303+
err.message.match(/selector engine has been already registered/)
304+
) {
305+
// ignore
306+
} else {
307+
throw err;
308+
}
309+
}
303310
}
304311

305312
/**

0 commit comments

Comments
 (0)