garrytan/gstack · error · BrowseClientError

browse ${args[0] || "unknown"} exited ${exitCode}: ${stderr}

Error message

browse ${args[0] || "unknown"} exited ${exitCode}: ${stderr}

What it means

Generic BrowseClientError thrown by runBrowse() when a browse subcommand (any args[0]) exits non-zero. It captures the child's exit code (defaulting to 1) and stderr so callers see which browse verb failed and why. This wraps any underlying browse daemon error (timeout, render failure, protocol mismatch, wedged tab).

Source

Thrown at make-pdf/src/browseClient.ts:188

 * Throws BrowseClientError on non-zero exit.
 */
function runBrowse(args: string[]): string {
  const bin = resolveBrowseBin();
  try {
    return execFileSync(bin, args, {
      encoding: "utf8",
      maxBuffer: 16 * 1024 * 1024,    // 16MB; tab content can be large
      stdio: ["ignore", "pipe", "pipe"],
      // A wedged daemon (or a hostile mermaid source spinning the renderer)
      // must fail the run, not hang it forever.
      timeout: 120_000,
    });
  } catch (err: any) {
    const exitCode = typeof err.status === "number" ? err.status : 1;
    const stderr = typeof err.stderr === "string"
      ? err.stderr
      : (err.stderr?.toString() ?? "");
    throw new BrowseClientError(exitCode, args[0] || "unknown", stderr);
  }
}

/**
 * Write a payload to a tmp file and return the path. Used for any payload
 * >4KB to avoid Windows argv limits (Codex round 2 #3).
 *
 * Path must be under the browse safe-dirs allowlist (/tmp or cwd on
 * non-Windows; os.tmpdir on Windows).  v1.6.0.0 tightened --from-file
 * validation to close a CLI/API parity gap (PR #1103), so os.tmpdir()
 * on macOS (/var/folders/...) now fails validateReadPath.  Use the same
 * TEMP_DIR convention as browse/src/platform.ts.
 */
const PAYLOAD_TMP_DIR = process.platform === "win32" ? os.tmpdir() : "/tmp";

function writePayloadFile(payload: Record<string, unknown>): string {
  const hash = crypto.createHash("sha256")
    .update(JSON.stringify(payload))

View on GitHub (pinned to 94993f7401)

Solutions

  1. Kill any stale browse daemon (`pkill -f browse` or via `browse kill`) and retry.
  2. Reproduce with the exact browse command from the error to read Chromium's stderr.
  3. Update browse and make-pdf together so their protocol matches (re-run ./setup).
  4. If a specific page spins the renderer, isolate it and reduce payload size or simplify the markup.

Example fix

// before
try {
  return execFileSync(bin, args, { encoding:'utf8', maxBuffer:16*1024*1024, stdio:['ignore','pipe','pipe'], timeout:120_000 });
} catch (err: any) {
  const exitCode = typeof err.status === 'number' ? err.status : 1;
  const stderr = typeof err.stderr === 'string' ? err.stderr : (err.stderr?.toString() ?? '');
  throw new BrowseClientError(exitCode, args[0] || 'unknown', stderr);
}

// after: also surface signal (timeout) and spawn error distinctly
} catch (err: any) {
  if (err.signal === 'SIGTERM') throw new BrowseClientError(124, args[0]||'unknown', `timeout after 120s: ${err.stderr?.toString()??''}`);
  if (err.code === 'ENOENT') throw new BrowseClientError(127, args[0]||'unknown', `browse binary missing: ${err.message}`);
  const exitCode = typeof err.status === 'number' ? err.status : 1;
  const stderr = typeof err.stderr === 'string' ? err.stderr : (err.stderr?.toString() ?? '');
  throw new BrowseClientError(exitCode, args[0] || 'unknown', stderr);
}
Defensive patterns

Strategy: try-catch

Type guard

import { BrowseClientError } from './browseClient';
function isBrowseError(e: unknown): e is BrowseClientError {
  return e instanceof BrowseClientError;
}

Try / catch

try {
  const tabId = newtab();
  // ... work ...
} catch (e) {
  if (e instanceof BrowseClientError) {
    if (e.command === 'newtab' && /timeout|wedged/i.test(e.message)) {
      // restart daemon once, then retry
      runBrowse(['kill']);
      throw e; // let the caller decide on a full re-run
    }
  }
  throw e;
}

Prevention

When it happens

Trigger: Any call through runBrowse — newtab, closetab, loadHtml, js, screenshot, pdf — where execFileSync throws because browse exited non-zero or hit the 120s timeout. Causes: a wedged Chromium daemon, a hostile page spinning the renderer past 120s, a browse/Chromium protocol version mismatch, a tab id that no longer exists, or invalid arguments.

Common situations: Mermaid/CJK content that crashes the renderer; a daemon left running from a killed make-pdf process (stale tab ids); browse binary newer/older than the make-pdf client expects; a 16MB maxBuffer overflow on huge tab content; a hostile HTML payload causing a renderer crash.

Related errors


AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12). Data as JSON: /api/errors/9ab58df01b3dda23. Report an issue: GitHub.