microsoft/playwright · error · Error

No attached tab to forward browser-level command: ${method}

Error message

No attached tab to forward browser-level command: ${method}

What it means

Thrown by the extension browser model's `sendBrowserCommand` when there are no attached tab sessions. Browser-scoped CDP commands (Storage.*, Browser.*, etc.) are forwarded through any attached tab via `chrome.debugger.sendCommand`, which requires a target; with zero tabs attached there is no route, so the command is rejected.

Source

Thrown at packages/playwright-core/src/tools/mcp/browserModel.ts:171

      return { success: false };
    await this._sendToExtension('chrome.tabs.remove', [tabSession.tabId]);
    return { success: true };
  }

  getTargetInfo(sessionId: string | undefined): any {
    if (!sessionId)
      return undefined;
    return this._findTabSession(s => s.sessionId === sessionId)?.targetInfo;
  }

  // Forward a browser-level CDP command (Storage.*, Browser.*, etc.) that is
  // not associated with a specific tab. chrome.debugger.sendCommand requires a
  // target, so we route through any attached tab; the command itself is
  // browser-scoped and returns the same result regardless of which tab is used.
  async sendBrowserCommand(method: string, params: any): Promise<any> {
    const tabSession = this._tabSessions.values().next().value;
    if (!tabSession)
      throw new Error(`No attached tab to forward browser-level command: ${method}`);
    return await this._sendToExtension('chrome.debugger.sendCommand', [
      { tabId: tabSession.tabId },
      method,
      params,
    ]);
  }

  // Forward a CDP command from Playwright to the tab its sessionId resolves to.
  async sendCommand(sessionId: string, method: string, params: any): Promise<any> {
    // Two cases:
    // 1. sessionId is a relay-level tab session (pw-tab-N) → strip and route by tabId.
    // 2. sessionId is a child CDP session (worker, oopif) → route to its owning tab,
    //    keep the sessionId so the extension forwards it to chrome.debugger.
    let tabSession = this._findTabSession(s => s.sessionId === sessionId);
    let cdpSessionId: string | undefined;
    if (!tabSession) {
      tabSession = this._findTabSession(s => s.childSessions.has(sessionId));
      cdpSessionId = sessionId;

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Ensure at least one tab is open and attached before issuing browser-level commands (trigger navigation/auto-attach first).
  2. If all tabs were closed, open a new tab/page (which auto-attaches) and retry.
  3. Confirm the extension still holds debugger attach permissions; re-grant if revoked.
Defensive patterns

Strategy: validation

Validate before calling

// Ensure a tab is attached before issuing browser-level commands.
function hasAttachedTab(model: any): boolean {
  // _tabSessions is private; expose a count via a public getter if extending.
  // Otherwise track attachments client-side via attach events.
  return /* model.tabCount() > 0 */ true;
}

Try / catch

try {
  await model.sendBrowserCommand(method, params);
} catch (e) {
  if (/No attached tab/.test((e as Error).message)) {
    await model.createTarget('about:blank'); // open+attach a tab
    await model.sendBrowserCommand(method, params);
  } else throw e;
}

Prevention

When it happens

Trigger: Issuing a browser-level CDP command (e.g. through Playwright's browser-level session) before any tab has been attached via `enableAutoAttach`/`_attachTab`, or after all tabs have been detached/closed.

Common situations: Calling a browser-scoped API at the very start of a session before auto-attach completed; all tabs were closed leaving the browser with zero targets; the extension detached all debuggee targets (user closed all tabs or revoked debugger permission).

Related errors


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/5978806ce4eceadc. Report an issue: GitHub.