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
- Kill any stale browse daemon (`pkill -f browse` or via `browse kill`) and retry.
- Reproduce with the exact browse command from the error to read Chromium's stderr.
- Update browse and make-pdf together so their protocol matches (re-run ./setup).
- 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
- Restart the browse daemon at the start of a long batch (`browse kill` then re-open).
- Keep make-pdf and browse versions in sync (re-run ./setup after either upgrades).
- Cap payload sizes before handing them to browse to avoid maxBuffer/argv limits.
- Log the failing command from e.command so retries are targeted.
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
- pdftotext failed on ${pdfPath}: ${err.message}
- 127
- 1
- diagram-render bundle did not become ready in the browse tab
- Usage: browse tab <id>
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/9ab58df01b3dda23.
Report an issue: GitHub.