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
- 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).
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
- 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).
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Timed out waiting for local Bun server on ${baseUrl}
- Timed out waiting for local port ${port}
- Timed out waiting for ${browser} automation lock
- Timed out waiting for report from ${browser} (last phase: ${
- Timed out waiting for posted report from ${browser} (last ph
AI-assisted analysis of chenglou/pretext@ac49b09b7d (2026-08-12).
Data as JSON: /api/errors/e61aa7d97b40b86f.
Report an issue: GitHub.