Merge pull request #33 from hobostay/fix/cdp-attach-rollback-page-enable-failure

fix(extension): roll back CDP attach when a domain enable fails
This commit is contained in:
Zhang GH
2026-08-11 14:38:12 +08:00
committed by GitHub
2 changed files with 44 additions and 4 deletions
@@ -67,6 +67,30 @@ describe("ChromiumCdp", () => {
expect(cdp.isAttached(5)).toBe(false);
});
it("rolls back the raw attach when a domain enable fails so the tab is not left stuck", async () => {
const { api } = fakeApi();
(api.sendCommand as ReturnType<typeof vi.fn>).mockImplementation(
async (_target, method: string) => {
if (method === "Page.enable") throw new Error("Page.enable rejected");
return {};
},
);
const cdp = new ChromiumCdp(api);
// Raw attach succeeds, but Page.enable fails. Without the rollback
// the debugger would stay attached while `attachedTabs` omits the id,
// leaving the tab stuck on "Another debugger is already attached" for
// every later call until the extension is reloaded.
await expect(cdp.ensureAttached(42)).rejects.toThrow(/Page\.enable rejected/);
expect(cdp.isAttached(42)).toBe(false);
expect(api.detach).toHaveBeenCalledWith({ tabId: 42 });
// The rollback detached, so a fresh attach can succeed afterwards.
(api.sendCommand as ReturnType<typeof vi.fn>).mockResolvedValue({});
await expect(cdp.ensureAttached(42)).resolves.toBeUndefined();
expect(cdp.isAttached(42)).toBe(true);
});
it("send() rejects on chrome.runtime.lastError-style failures", async () => {
const { api } = fakeApi();
(api.sendCommand as ReturnType<typeof vi.fn>).mockImplementation(
@@ -172,10 +172,26 @@ export class ChromiumCdp {
}
const attach = (async () => {
await this.api.attach({ tabId }, CDP_PROTOCOL_VERSION);
await this.enablePageDomain(tabId);
await this.enableConsoleDomains(tabId);
await this.enableNetworkDomainBestEffort(tabId);
this.attachedTabs.add(tabId);
try {
await this.enablePageDomain(tabId);
await this.enableConsoleDomains(tabId);
await this.enableNetworkDomainBestEffort(tabId);
this.attachedTabs.add(tabId);
} catch (err) {
// A CDP domain enable failed after the raw attach succeeded
// (e.g. `Page.enable` rejects because the tab just navigated to
// a chrome:// URL or the Web Store). `attachedTabs` does not yet
// hold `tabId`, so without rolling back the debugger would stay
// attached and the next `ensureAttached` would re-attach and hit
// "Another debugger is already attached" forever — the tab is
// unusable until the extension is reloaded. Detach directly
// (`this.detach()` is a no-op here because `attachedTabs` lacks
// the id) so the next attempt starts from a clean slate.
await this.api.detach({ tabId }).catch((detachErr) => {
console.debug("[bsk cdp] rollback detach failed", { tabId, detachErr });
});
throw err;
}
})()
.catch((err) => {
// Chrome surfaces "Another debugger is already attached" when