chenglou/pretext · error · Error

Timed out waiting for local Bun server on port ${port}${path

Error message

Timed out waiting for local Bun server on port ${port}${pathname}

What it means

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).

Source

Thrown at scripts/browser-automation.ts:627

  if (existingBaseUrl !== null) {
    return { baseUrl: existingBaseUrl, process: null }
  }

  const serverProcess = spawn('/bin/zsh', ['-lc', `bun --port=${port} --no-hmr pages/*.html`], {
    cwd,
    stdio: 'ignore',
  })

  const start = Date.now()
  while (Date.now() - start < 20_000) {
    const baseUrl = await resolveBaseUrl(port, pathname)
    if (baseUrl !== null) {
      return { baseUrl, process: serverProcess }
    }
    await sleep(100)
  }

  throw new Error(`Timed out waiting for local Bun server on port ${port}${pathname}`)
}

export async function loadHashReport<T extends { requestId?: string }>(
  session: BrowserSession,
  url: string,
  expectedRequestId: string,
  browser: BrowserKind,
  timeoutMs = 60_000,
): Promise<T> {
  await session.navigate(url)

  const attempts = Math.max(1, Math.ceil(timeoutMs / 100))
  let lastPhase: NavigationPhase | null = null
  for (let i = 0; i < attempts; i++) {
    await sleep(100)
    const currentUrl = await session.readLocationUrl()
    const phase = readNavigationPhaseState(currentUrl)
    if (

View on GitHub (pinned to ac49b09b7d)

Solutions

  1. Run `bun install` in the repo root before invoking any script that calls ensurePageServer — most startup failures are missing node_modules.
  2. 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.
  3. Confirm you are invoking the script from the repo root (ensurePageServer uses process.cwd() for both the Bun cwd and the pages glob).
  4. Free the target port (lsof -i :<port>) or pass --port=0 via the script's --port flag so getAvailablePort picks a free one.
  5. 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).

Example fix

// before
const serverProcess = spawn('/bin/zsh', ['-lc', `bun --port=${port} --no-hmr pages/*.html`], {
  cwd,
  stdio: 'ignore',
})
const start = Date.now()
while (Date.now() - start < 20_000) {
  const baseUrl = await resolveBaseUrl(port, pathname)
  if (baseUrl !== null) return { baseUrl, process: serverProcess }
  await sleep(100)
}
throw new Error(`Timed out waiting for local Bun server on port ${port}${pathname}`)
// after — surface Bun's own stderr so the cause is not hidden, and reap it on timeout
const serverProcess = spawn('/bin/zsh', ['-lc', `bun --port=${port} --no-hmr pages/*.html`], {
  cwd,
  stdio: ['ignore', 'pipe', 'pipe'],
})
let stderrBuf = ''
serverProcess.stderr?.on('data', chunk => { stderrBuf += chunk })
const start = Date.now()
while (Date.now() - start < 20_000) {
  const baseUrl = await resolveBaseUrl(port, pathname)
  if (baseUrl !== null) return { baseUrl, process: serverProcess }
  await sleep(100)
}
serverProcess.kill()
throw new Error(`Bun server on port ${port}${pathname} did not answer in 20s. Bun stderr: ${stderrBuf.slice(0, 500)}`)
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight the conditions ensurePageServer depends on
import { existsSync } from 'node:fs'
import path from 'node:path'
function preflightPageServer(cwd: string, port: number): void {
  if (!existsSync(path.join(cwd, 'node_modules'))) throw new Error('Run `bun install` first — page server needs dependencies.')
  const pages = path.join(cwd, 'pages')
  if (!existsSync(pages)) throw new Error(`No pages/ directory in ${cwd}; ensurePageServer globs pages/*.html.`)
  //bun must be reachable from a non-interactive shell
  const which = Bun.spawnSync(['which', 'bun'], { stdout: 'static', stderr: 'static' }).stdout.toString().trim()
  if (!which) throw new Error('`bun` not on PATH for non-interactive shells; symlink it into /usr/local/bin.')
}

Try / catch

try {
  const pageServer = await ensurePageServer(port, '/corpus', process.cwd())
} catch (error) {
  if (error instanceof Error && error.message.startsWith('Timed out waiting for local Bun server')) {
    // Reproduce manually to see the hidden startup error:
    //   bun --port=<port> --no-hmr pages/*.html
    throw new Error(`${error.message}. Reproduce with: bun --port=${port} --no-hmr pages/*.html`)
  }
  throw error
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of chenglou/pretext@ac49b09b7d (2026-08-12). Data as JSON: /api/errors/e61aa7d97b40b86f. Report an issue: GitHub.