can1357/oh-my-pi · info · ToolAbortError

Browser open aborted

Error message

Browser open aborted

What it means

acquireBrowser() checks opts.signal?.aborted immediately before launching a browser (and again after the launch resolves). If the caller's AbortSignal already fired, it throws ToolAbortError('Browser open aborted') so no Chromium process is ever spawned for an already-cancelled request. This is deliberate, expected cancellation flow, not a fault in the browser layer.

Source

Thrown at packages/coding-agent/src/tools/browser/registry.ts:119

	appArgs?: string[];
	signal?: AbortSignal;
}

export async function acquireBrowser(kind: BrowserKind, opts: AcquireBrowserOptions): Promise<BrowserHandle> {
	const key = browserKey(kind);
	for (;;) {
		const existing = browsers.get(key);
		if (existing) {
			if ("client" in existing) return existing;
			if (existing.browser.connected) return existing;
			browsers.delete(key);
			await disposeBrowserHandle(existing, { kill: false });
			continue;
		}
		// Short-circuit before launching: the tool wrapper's `untilAborted` only
		// rejects its outer promise on abort; without this check `openBrowserHandle`
		// would still fire and its result would land in `browsers` below.
		if (opts.signal?.aborted) throw new ToolAbortError("Browser open aborted");

		// Single-flight per key: a concurrent caller already opening this browser
		// wins; everyone else waits and re-reads the registry. Without this, N
		// simultaneous opens each launch a Chromium and the last write wins,
		// leaking the rest as unreferenced process trees.
		const pending = pendingOpens.get(key);
		if (pending) {
			await pending.catch(() => undefined);
			continue;
		}
		const open = openBrowserHandle(kind, opts).finally(() => pendingOpens.delete(key));
		pendingOpens.set(key, open);
		const handle = await open;
		// The launch may resolve AFTER the caller has already aborted (the outer
		// `untilAborted` rejects immediately on abort but does not cancel the
		// inner promise, and `launchHeadlessBrowser` does not accept a signal).
		// Without this branch the completed handle sits in `browsers` at
		// refCount:0 forever — no tab ever takes a hold, `releaseBrowser` never

View on GitHub (pinned to 9690622007)

Solutions

  1. No fix needed — handle it as cancellation: catch ToolAbortError and return a 'cancelled' result instead of treating it as a browser failure
  2. If aborts are spurious, check what supplies opts.signal (tool wrapper untilAborted / timeout) and raise the timeout or avoid aborting that call
  3. Clean up is automatic; nothing to dispose manually — the registry disposes any orphaned launch itself

Example fix

// before: treating abort as a crash
try { await acquireBrowser(kind, { signal }); } catch (e) { logger.error(e); }
// after
try { await acquireBrowser(kind, { signal }); }
catch (e) {
  if (e instanceof ToolAbortError) return { status: "cancelled" };
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (signal?.aborted) {
  // don't call acquireBrowser at all
  return cancelledResult;
}

Type guard

function isAbort(err: unknown): err is ToolAbortError {
  return err instanceof ToolAbortError;
}

Try / catch

try {
  const handle = await acquireBrowser(kind, { cwd, signal });
} catch (err) {
  if (isAbort(err)) return { cancelled: true }; // expected cancellation, not a browser failure
  throw err;
}

Prevention

When it happens

Trigger: The tool call's AbortSignal was cancelled (user pressed escape / request aborted) while the registry was still iterating existing handles or just before openBrowserHandle() was invoked; also thrown post-launch if the signal aborted while the launch was in flight (line 141-148), with the orphan handle disposed first.

Common situations: User cancels a browser_open tool call while the (slow) launch is starting; timeouts cancelling the signal during first-use Chromium download+launch; parallel tool calls aborted together.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/8de82f056129791a. Report an issue: GitHub.