{"record":{"id":"e61aa7d97b40b86f","repo":"chenglou/pretext","slug":"timed-out-waiting-for-local-bun-server-on-port-p","errorCode":null,"errorMessage":"Timed out waiting for local Bun server on port ${port}${pathname}","messagePattern":"Timed out waiting for local Bun server on port (.+?)(.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/browser-automation.ts","lineNumber":627,"sourceCode":"  if (existingBaseUrl !== null) {\n    return { baseUrl: existingBaseUrl, process: null }\n  }\n\n  const serverProcess = spawn('/bin/zsh', ['-lc', `bun --port=${port} --no-hmr pages/*.html`], {\n    cwd,\n    stdio: 'ignore',\n  })\n\n  const start = Date.now()\n  while (Date.now() - start < 20_000) {\n    const baseUrl = await resolveBaseUrl(port, pathname)\n    if (baseUrl !== null) {\n      return { baseUrl, process: serverProcess }\n    }\n    await sleep(100)\n  }\n\n  throw new Error(`Timed out waiting for local Bun server on port ${port}${pathname}`)\n}\n\nexport async function loadHashReport<T extends { requestId?: string }>(\n  session: BrowserSession,\n  url: string,\n  expectedRequestId: string,\n  browser: BrowserKind,\n  timeoutMs = 60_000,\n): Promise<T> {\n  await session.navigate(url)\n\n  const attempts = Math.max(1, Math.ceil(timeoutMs / 100))\n  let lastPhase: NavigationPhase | null = null\n  for (let i = 0; i < attempts; i++) {\n    await sleep(100)\n    const currentUrl = await session.readLocationUrl()\n    const phase = readNavigationPhaseState(currentUrl)\n    if (","sourceCodeStart":609,"sourceCodeEnd":645,"githubUrl":"https://github.com/chenglou/pretext/blob/ac49b09b7d83ede19581fa94a8b892b07d309baf/scripts/browser-automation.ts#L609-L645","documentation":"Thrown by ensurePageServer after it spawns `bun --port=N --no-hmr pages/*.html` (via a zsh login shell) and then polls resolveBaseUrl(port, pathname) every 100ms for up to 20 seconds. If the spawned Bun server never becomes reachable at the expected host:port within 20s, the harness gives up. The Bun process is left spawned (caller does not kill it on this path).","triggerScenarios":"spawn('/bin/zsh', ['-lc', `bun --port=${port} --no-hmr pages/*.html`], { cwd, stdio: 'ignore' }) starts but the server never answers resolveBaseUrl. Causes: `bun` is not on PATH in a non-interactive zsh login shell (no nvm/fnm shim sourced); pages/*.html glob expanded to nothing because cwd has no pages dir; a page module throws at import time so Bun exits non-zero immediately; the chosen port is already taken so Bun fails to bind (and resolveBaseUrl hits the wrong server or nothing); stdio:'ignore' hides a startup error.","commonSituations":"Fresh worktree where `bun install` was not run so a dependency import fails at page boot; running the script from a cwd other than the repo root (pages/*.html not found); a stale Bun server from a previous run still holds the port; a transient port conflict because getAvailablePort returned a port that was taken between reservation and spawn; zsh login-shell PATH differs from the user's interactive shell so the `bun` binary is missing.","solutions":["Run `bun install` in the repo root before invoking any script that calls ensurePageServer — most startup failures are missing node_modules.","Reproduce the spawn manually from the same cwd: `bun --port=<port> --no-hmr pages/*.html` — the error that stdio:'ignore' hid will print to the terminal.","Confirm you are invoking the script from the repo root (ensurePageServer uses process.cwd() for both the Bun cwd and the pages glob).","Free the target port (lsof -i :<port>) or pass --port=0 via the script's --port flag so getAvailablePort picks a free one.","If `bun` is not found, ensure it is on PATH for non-interactive shells (symlink in /usr/local/bin or source the version manager in /etc/zshenv)."],"exampleFix":"// before\nconst serverProcess = spawn('/bin/zsh', ['-lc', `bun --port=${port} --no-hmr pages/*.html`], {\n  cwd,\n  stdio: 'ignore',\n})\nconst start = Date.now()\nwhile (Date.now() - start < 20_000) {\n  const baseUrl = await resolveBaseUrl(port, pathname)\n  if (baseUrl !== null) return { baseUrl, process: serverProcess }\n  await sleep(100)\n}\nthrow new Error(`Timed out waiting for local Bun server on port ${port}${pathname}`)\n// after — surface Bun's own stderr so the cause is not hidden, and reap it on timeout\nconst serverProcess = spawn('/bin/zsh', ['-lc', `bun --port=${port} --no-hmr pages/*.html`], {\n  cwd,\n  stdio: ['ignore', 'pipe', 'pipe'],\n})\nlet stderrBuf = ''\nserverProcess.stderr?.on('data', chunk => { stderrBuf += chunk })\nconst start = Date.now()\nwhile (Date.now() - start < 20_000) {\n  const baseUrl = await resolveBaseUrl(port, pathname)\n  if (baseUrl !== null) return { baseUrl, process: serverProcess }\n  await sleep(100)\n}\nserverProcess.kill()\nthrow new Error(`Bun server on port ${port}${pathname} did not answer in 20s. Bun stderr: ${stderrBuf.slice(0, 500)}`)","handlingStrategy":"validation","validationCode":"// Pre-flight the conditions ensurePageServer depends on\nimport { existsSync } from 'node:fs'\nimport path from 'node:path'\nfunction preflightPageServer(cwd: string, port: number): void {\n  if (!existsSync(path.join(cwd, 'node_modules'))) throw new Error('Run `bun install` first — page server needs dependencies.')\n  const pages = path.join(cwd, 'pages')\n  if (!existsSync(pages)) throw new Error(`No pages/ directory in ${cwd}; ensurePageServer globs pages/*.html.`)\n  //bun must be reachable from a non-interactive shell\n  const which = Bun.spawnSync(['which', 'bun'], { stdout: 'static', stderr: 'static' }).stdout.toString().trim()\n  if (!which) throw new Error('`bun` not on PATH for non-interactive shells; symlink it into /usr/local/bin.')\n}","typeGuard":null,"tryCatchPattern":"try {\n  const pageServer = await ensurePageServer(port, '/corpus', process.cwd())\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith('Timed out waiting for local Bun server')) {\n    // Reproduce manually to see the hidden startup error:\n    //   bun --port=<port> --no-hmr pages/*.html\n    throw new Error(`${error.message}. Reproduce with: bun --port=${port} --no-hmr pages/*.html`)\n  }\n  throw error\n}","preventionTips":["Always run `bun install` in a fresh worktree before any script that calls ensurePageServer.","Invoke these scripts from the repo root — ensurePageServer keys both the spawn cwd and the pages glob off process.cwd().","Free the requested port before running (lsof -i :<port>) or rely on --port=0 to let getAvailablePort pick.","Keep `bun` on a PATH that non-interactive zsh login shells see (symlink or /etc/zshenv)."],"tags":["browser-automation","server","timeout","bun","spawn"],"backgroundTag":null,"analyzedSha":"ac49b09b7d83ede19581fa94a8b892b07d309baf","analyzedAt":"2026-08-12T17:03:16.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}