{"record":{"id":"9ab58df01b3dda23","repo":"garrytan/gstack","slug":"browse-args-0-unknown-exited-exitcode","errorCode":null,"errorMessage":"browse ${args[0] || \"unknown\"} exited ${exitCode}: ${stderr}","messagePattern":"browse (.+?) exited (.+?): (.+?)","errorType":"exception","errorClass":"BrowseClientError","httpStatus":null,"severity":"error","filePath":"make-pdf/src/browseClient.ts","lineNumber":188,"sourceCode":" * Throws BrowseClientError on non-zero exit.\n */\nfunction runBrowse(args: string[]): string {\n  const bin = resolveBrowseBin();\n  try {\n    return execFileSync(bin, args, {\n      encoding: \"utf8\",\n      maxBuffer: 16 * 1024 * 1024,    // 16MB; tab content can be large\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      // A wedged daemon (or a hostile mermaid source spinning the renderer)\n      // must fail the run, not hang it forever.\n      timeout: 120_000,\n    });\n  } catch (err: any) {\n    const exitCode = typeof err.status === \"number\" ? err.status : 1;\n    const stderr = typeof err.stderr === \"string\"\n      ? err.stderr\n      : (err.stderr?.toString() ?? \"\");\n    throw new BrowseClientError(exitCode, args[0] || \"unknown\", stderr);\n  }\n}\n\n/**\n * Write a payload to a tmp file and return the path. Used for any payload\n * >4KB to avoid Windows argv limits (Codex round 2 #3).\n *\n * Path must be under the browse safe-dirs allowlist (/tmp or cwd on\n * non-Windows; os.tmpdir on Windows).  v1.6.0.0 tightened --from-file\n * validation to close a CLI/API parity gap (PR #1103), so os.tmpdir()\n * on macOS (/var/folders/...) now fails validateReadPath.  Use the same\n * TEMP_DIR convention as browse/src/platform.ts.\n */\nconst PAYLOAD_TMP_DIR = process.platform === \"win32\" ? os.tmpdir() : \"/tmp\";\n\nfunction writePayloadFile(payload: Record<string, unknown>): string {\n  const hash = crypto.createHash(\"sha256\")\n    .update(JSON.stringify(payload))","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/garrytan/gstack/blob/94993f74012782fd94416dd44b8314f6363a13a4/make-pdf/src/browseClient.ts#L170-L206","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\ntry {\n  return execFileSync(bin, args, { encoding:'utf8', maxBuffer:16*1024*1024, stdio:['ignore','pipe','pipe'], timeout:120_000 });\n} catch (err: any) {\n  const exitCode = typeof err.status === 'number' ? err.status : 1;\n  const stderr = typeof err.stderr === 'string' ? err.stderr : (err.stderr?.toString() ?? '');\n  throw new BrowseClientError(exitCode, args[0] || 'unknown', stderr);\n}\n\n// after: also surface signal (timeout) and spawn error distinctly\n} catch (err: any) {\n  if (err.signal === 'SIGTERM') throw new BrowseClientError(124, args[0]||'unknown', `timeout after 120s: ${err.stderr?.toString()??''}`);\n  if (err.code === 'ENOENT') throw new BrowseClientError(127, args[0]||'unknown', `browse binary missing: ${err.message}`);\n  const exitCode = typeof err.status === 'number' ? err.status : 1;\n  const stderr = typeof err.stderr === 'string' ? err.stderr : (err.stderr?.toString() ?? '');\n  throw new BrowseClientError(exitCode, args[0] || 'unknown', stderr);\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"import { BrowseClientError } from './browseClient';\nfunction isBrowseError(e: unknown): e is BrowseClientError {\n  return e instanceof BrowseClientError;\n}","tryCatchPattern":"try {\n  const tabId = newtab();\n  // ... work ...\n} catch (e) {\n  if (e instanceof BrowseClientError) {\n    if (e.command === 'newtab' && /timeout|wedged/i.test(e.message)) {\n      // restart daemon once, then retry\n      runBrowse(['kill']);\n      throw e; // let the caller decide on a full re-run\n    }\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["make-pdf","browse","subprocess","execfilesync","daemon"],"backgroundTag":null,"analyzedSha":"94993f74012782fd94416dd44b8314f6363a13a4","analyzedAt":"2026-08-12T04:06:23.140Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}