chatboxai/chatbox · error · Error

Failed to start preview server

Error message

Failed to start preview server

What it means

Thrown by the preview server's start routine in two spots: (1) inside the listen callback if server.address() is missing or not an object (reject), and (2) after the await if port is still null (the listen callback never resolved). '127.0.0.1' binding with port 0 (OS-assigned) is the contract; failure means no port could be bound or the address wasn't returned in time.

Source

Thrown at src/main/sandbox/preview-server.ts:173

  server = createServer((req, res) => {
    void handleRequest(req, res)
  })

  await new Promise<void>((resolve, reject) => {
    server?.once('error', reject)
    server?.listen(0, '127.0.0.1', () => {
      const address = server?.address()
      if (address && typeof address === 'object') {
        port = address.port
        resolve()
      } else {
        reject(new Error('Failed to start preview server'))
      }
    })
  })

  if (port === null) throw new Error('Failed to start preview server')
  return port
}

export async function createSandboxHtmlPreviewUrl(
  filePath: string
): Promise<{ success: boolean; url?: string; error?: string }> {
  try {
    const sandboxRoots = getSandboxRoots()
    const fileStat = await lstat(filePath)
    if (fileStat.isSymbolicLink()) {
      return { success: false, error: 'Access denied: symlinks not allowed' }
    }
    const resolvedPath = await realpath(filePath)
    const sandboxRoot = sandboxRoots.find((root) => isInside(root, resolvedPath))
    if (!sandboxRoot) {
      return { success: false, error: 'Access denied: path outside sandbox directory' }
    }
    if (!['.html', '.htm'].includes(path.extname(resolvedPath).toLowerCase())) {

View on GitHub (pinned to 81571269ad)

Solutions

  1. Attach a listener for the 'error' event on the server BEFORE listen — EADDRINUSE/EACCES surface there and currently reject (the first reject wins). Surface that error specifically rather than the generic 'Failed to start preview server'.
  2. Retry start() a few times with backoff to absorb transient bind races when port 0 collides.
  3. In sandboxed runtimes, confirm 127.0.0.1 is available; if not, the preview feature must be disabled for that environment.
  4. Ensure the server instance is freshly created per start attempt (a closed server will not emit listen).

Example fix

// before
server?.once('error', reject)
server?.listen(0, '127.0.0.1', () => { ... else reject(new Error('Failed to start preview server')) })

// after: include the underlying error cause
server?.once('error', (err) => reject(new Error('Failed to start preview server', { cause: err })))
Defensive patterns

Strategy: retry

Validate before calling

if (server.listening) throw new Error('Preview server already started')

Type guard

function isPreviewStartFailure(e: unknown): e is Error { return e instanceof Error && e.message === 'Failed to start preview server' }

Try / catch

for (let attempt = 0; attempt < 3; attempt++) {
  try { return await startPreviewServer() } catch (e) { if (!isPreviewStartFailure(e) || attempt === 2) throw e; await delay(200 * 2 ** attempt) }
}

Prevention

When it happens

Trigger: server.listen(0,'127.0.0.1',cb) never calls cb with a valid AddressInfo (address undefined / not typeof 'object'), or the cb branch that resolves never ran so port stays null after the promise settles. Common when an 'error' event rejects the promise first, but also when listen silently fails.

Common situations: Another process already holds the randomly assigned port (rare with port 0 but possible during a race); the loopback interface is unavailable in a sandboxed/containerized env; the http.Server was already closed before listen fired; system fd/socket exhaustion.

Related errors


AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12). Data as JSON: /api/errors/a8eb74d9d2e2d6e5. Report an issue: GitHub.