mastra-ai/mastra · error · Error

Invalid browser configuration: "cdpUrl" and "scope: 'thread'

Error message

Invalid browser configuration: "cdpUrl" and "scope: 'thread'" cannot be used together.

• cdpUrl connects to a single existing browser instance (all threads share it)
• scope: "thread" requires spawning separate browser instances per thread

To fix this, either:
1. Remove cdpUrl to let the provider spawn separate browser instances (supports thread isolation)
2. Use scope: "shared" when connecting via cdpUrl (all threads share one browser)

What it means

The Browser constructor rejects configurations that combine cdpUrl (connect to one existing browser via Chrome DevTools Protocol) with scope: 'thread' (which requires spawning a separate browser per thread). TypeScript types prevent this at compile time, but a runtime check gives a clear message when the config is built dynamically in JS or via casts.

Source

Thrown at packages/core/src/browser/browser.ts:632

  private _closePromise?: Promise<void>;

  // ---------------------------------------------------------------------------
  // Constructor
  // ---------------------------------------------------------------------------

  constructor(config: BrowserConfig = {}) {
    super({ name: 'MastraBrowser', component: RegisteredLogger.BROWSER });
    this.config = config;

    // Validate configuration: cdpUrl and scope: 'thread' are mutually exclusive
    // When connecting to an external browser via cdpUrl, we connect to a single existing browser.
    // Thread isolation requires spawning separate browser instances, which isn't possible with cdpUrl.
    // Note: The BrowserConfig type enforces this at compile-time, but we keep this runtime check
    // for better error messages when users bypass TypeScript (e.g., from JavaScript or casting).
    // We capture scope before checking cdpUrl to avoid TypeScript narrowing the union type.
    const scope = config.scope;
    if (config.cdpUrl && scope === 'thread') {
      throw new Error(
        'Invalid browser configuration: "cdpUrl" and "scope: \'thread\'" cannot be used together.\n\n' +
          '• cdpUrl connects to a single existing browser instance (all threads share it)\n' +
          '• scope: "thread" requires spawning separate browser instances per thread\n\n' +
          'To fix this, either:\n' +
          '1. Remove cdpUrl to let the provider spawn separate browser instances (supports thread isolation)\n' +
          '2. Use scope: "shared" when connecting via cdpUrl (all threads share one browser)',
      );
    }

    // Validate: cdpUrl is incompatible with launch-time options (profile, executablePath).
    // CDP connects to an already-running browser — it has its own profile and executable.
    if (config.cdpUrl && (config.profile || config.executablePath)) {
      const conflicting = [config.profile && 'profile', config.executablePath && 'executablePath']
        .filter(Boolean)
        .join(' and ');
      throw new Error(
        `Invalid browser configuration: "cdpUrl" cannot be used with ${conflicting}.\n\n` +
          '• cdpUrl connects to an existing browser (which has its own profile and executable)\n' +

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Remove cdpUrl so the provider spawns separate browser instances per thread (keeps thread isolation).
  2. Change scope to 'shared' when connecting via cdpUrl, accepting that all threads share one browser.
  3. Make the config source mutually exclusive: only read cdpUrl when scope is 'shared' (or undefined).

Example fix

// before
const browser = new Browser({ cdpUrl: process.env.CDP_URL, scope: 'thread' }); // throws
// after
const browser = process.env.CDP_URL
  ? new Browser({ cdpUrl: process.env.CDP_URL, scope: 'shared' })
  : new Browser({ scope: 'thread' });
Defensive patterns

Strategy: validation

Validate before calling

function buildBrowserConfig(opts) {
  if (opts.cdpUrl && opts.scope === 'thread') {
    throw new Error('cdpUrl cannot be combined with scope "thread"; use scope "shared" or drop cdpUrl');
  }
  return opts;
}
// call site:
const browser = new Browser(buildBrowserConfig(rawOptions));

Type guard

function isValidBrowserConfig(config) {
  if (config.cdpUrl && config.scope === 'thread') return false;
  return true;
}

Try / catch

let browser;
try {
  browser = new Browser(config);
} catch (e) {
  if (e.message.includes('cdpUrl') && e.message.includes("scope: 'thread'")) {
    browser = new Browser({ ...config, scope: 'shared' });
  } else throw e;
}

Prevention

When it happens

Trigger: Creating a Browser with a config object containing both cdpUrl and scope: 'thread', typically assembled dynamically (from env vars, a DB record, or JS without type checking).

Common situations: Config sourced from environment/feature flags where cdpUrl is set for production while scope stays 'thread' from the default; JS callers bypassing the BrowserConfig discriminated-union types; copy-pasting cdpUrl examples into a thread-scoped setup.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/0a3d728f65878649. Report an issue: GitHub.