chenglou/pretext · error · Error

Timed out waiting for local port ${port}

Error message

Timed out waiting for local port ${port}

What it means

Thrown by waitForPort() in browser-automation.ts after 200 failed TCP connection attempts (100ms apart = 20s) to 127.0.0.1:<port>. Unlike waitForServer (error 4) which does HTTP fetches, this opens a raw net socket and checks for 'connect' vs 'error'. It is used to wait for WebDriver/BiDi debugging ports (not HTTP servers), so an HTTP-unreachable but TCP-open port still counts as ready. If the target process never listens within 20s, it throws.

Source

Thrown at scripts/browser-automation.ts:93

  for (let i = 0; i < 200; i++) {
    const open = await new Promise<boolean>(resolve => {
      const socket = createConnection({ host: '127.0.0.1', port })
      let settled = false

      const finish = (value: boolean): void => {
        if (settled) return
        settled = true
        socket.destroy()
        resolve(value)
      }

      socket.once('connect', () => finish(true))
      socket.once('error', () => finish(false))
    })
    if (open) return
    await sleep(100)
  }
  throw new Error(`Timed out waiting for local port ${port}`)
}

export async function getAvailablePort(requestedPort: number | null = null): Promise<number> {
  if (requestedPort !== null && Number.isFinite(requestedPort) && requestedPort > 0) {
    return requestedPort
  }

  return await new Promise((resolve, reject) => {
    const server = createNetServer()
    server.once('error', reject)
    server.listen(0, '127.0.0.1', () => {
      const address = server.address()
      if (address === null || typeof address === 'string') {
        reject(new Error('Failed to allocate a free port'))
        return
      }

      const { port } = address

View on GitHub (pinned to ac49b09b7d)

Solutions

  1. Confirm Firefox is installed at /Applications/Firefox.app/Contents/MacOS/firefox (the path is hardcoded at browser-automation.ts:360).
  2. Quit any running Firefox instance before running the script (--new-instance requires no existing process).
  3. Manually launch Firefox with the same flags to see the startup error.
  4. Clear stale profile dirs under mkdtemp's tmpdir prefix 'pretext-firefox-'.
Defensive patterns

Strategy: retry

Validate before calling

// Confirm the Firefox binary exists before spawning.
import { existsSync } from 'node:fs'
if (!existsSync('/Applications/Firefox.app/Contents/MacOS/firefox')) {
  throw new Error('Firefox not found at /Applications/Firefox.app')
}

Try / catch

// Add a retry around waitForPort for transient startup races.
async function waitForPortWithRetry(port: number, attempts = 3): Promise<void> {
  for (let i = 0; i < attempts; i++) {
    try { await waitForPort(port); return }
    catch { if (i === attempts - 1) throw }
  }
}

Prevention

When it happens

Trigger: The Firefox remote-debugging port (initializeFirefoxSession) never accepts connections within 20s — Firefox failed to launch, crashed on startup, or the port was wrong. Also any other caller of waitForPort whose target process did not bind in time.

Common situations: Firefox is not installed at the hardcoded /Applications/Firefox.app path; Firefox is already running and refusing --new-instance; the spawned profile is corrupted; macOS Gatekeeper quarantining the binary; heavy system load delaying Firefox startup beyond 20s.

Understand the failure class

Related errors


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