Skip to content

fix(extension): roll back CDP attach when a domain enable fails#33

Open
hobostay wants to merge 1 commit into
Tencent:mainfrom
hobostay:fix/cdp-attach-rollback-page-enable-failure
Open

fix(extension): roll back CDP attach when a domain enable fails#33
hobostay wants to merge 1 commit into
Tencent:mainfrom
hobostay:fix/cdp-attach-rollback-page-enable-failure

Conversation

@hobostay

Copy link
Copy Markdown

Problem

ChromiumCdp.ensureAttached can leave a tab permanently stuck when a CDP domain enable fails after the raw attach succeeds.

The attach sequence was:

await this.api.attach({ tabId }, CDP_PROTOCOL_VERSION); // succeeds -> Chrome has the debugger attached
await this.enablePageDomain(tabId);                     // can throw (Page.enable)
await this.enableConsoleDomains(tabId);                 // best-effort (swallows)
await this.enableNetworkDomainBestEffort(tabId);        // best-effort (swallows)
this.attachedTabs.add(tabId);                           // only reached if all of the above succeed

enablePageDomain is the only domain enable that is neither best-effort nor guarded. If it rejects — e.g. the tab navigates to a chrome:// URL or the Web Store in the gap between attach and Page.enable, or a transient CDP error during attach — the .catch re-throws but performs no cleanup, and attachedTabs never records the tabId.

On the next tool call, ensureAttached sees the tabId is not cached, has no in-flight attach, and calls api.attach again → Chrome rejects it with "Another debugger is already attached". Every subsequent operation on that tab fails until the extension is reloaded. Note the public detach() cannot self-heal this, because it no-ops when attachedTabs lacks the id.

Fix

Detach the debuggee directly in the failure path (before re-throwing) so the next attempt starts from a clean slate:

await this.api.attach({ tabId }, CDP_PROTOCOL_VERSION);
try {
  await this.enablePageDomain(tabId);
  await this.enableConsoleDomains(tabId);
  await this.enableNetworkDomainBestEffort(tabId);
  this.attachedTabs.add(tabId);
} catch (err) {
  await this.api.detach({ tabId }).catch((detachErr) => {
    console.debug("[bsk cdp] rollback detach failed", { tabId, detachErr });
  });
  throw err;
}

api.attach failures are untouched (no detach attempted, since nothing was attached) — only failures after a successful attach roll back.

Test

Adds a regression test that mocks Page.enable to throw after attach succeeds, then asserts:

  • ensureAttached rejects with the original error,
  • the rollback calls api.detach({ tabId }),
  • the tab is not left in the cache, and
  • a fresh ensureAttached succeeds afterwards.

This test fails on the previous code (0 detach calls) and passes with the fix.

Validation

Ran the frontend CI gates locally against this branch:

  • pnpm --filter @browser-skill/extension exec wxt prepare
  • pnpm ext:test — 388 passed ✅
  • pnpm --filter @browser-skill/extension compile (tsc --noEmit) ✅
  • pnpm lint (biome + stylelint) ✅
  • pnpm ext:build

🤖 Generated with Claude Code

If `chrome.debugger.attach` succeeds but a subsequent CDP domain enable
rejects — most notably `Page.enable`, when the tab navigates to a
chrome:// URL or the Web Store in the gap between the two calls — the
driver left the tab in an unrecoverable state. `attachedTabs` never
recorded the tabId, so the next `ensureAttached` re-attached and failed
permanently with "Another debugger is already attached" until the
extension was reloaded.

Detach the debuggee directly in the failure path (the public `detach()`
is a no-op here because the id is not cached yet) so the next attempt
starts from a clean slate. Adds a regression test that fails on the
previous code, where the rollback never ran and `detach` was never
called.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant