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
- Remove cdpUrl so the provider spawns separate browser instances per thread (keeps thread isolation).
- Change scope to 'shared' when connecting via cdpUrl, accepting that all threads share one browser.
- 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
- Centralize browser config construction in one typed helper so TS unions block bad combos.
- Never spread raw env/DB values into BrowserConfig without normalizing scope first.
- When cdpUrl is present, force scope to 'shared' by default.
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
- Invalid browser configuration: "cdpUrl" cannot be used with
- Google RBAC roleMapping is required.
- Cookie password must be at least 32 characters. Set OKTA_COO
- browser_close requires agent.threadId when browser scope is
- No browser context available for screencast
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/0a3d728f65878649.
Report an issue: GitHub.