{"record":{"id":"a8eb74d9d2e2d6e5","repo":"chatboxai/chatbox","slug":"failed-to-start-preview-server","errorCode":null,"errorMessage":"Failed to start preview server","messagePattern":"Failed to start preview server","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/sandbox/preview-server.ts","lineNumber":173,"sourceCode":"\n  server = createServer((req, res) => {\n    void handleRequest(req, res)\n  })\n\n  await new Promise<void>((resolve, reject) => {\n    server?.once('error', reject)\n    server?.listen(0, '127.0.0.1', () => {\n      const address = server?.address()\n      if (address && typeof address === 'object') {\n        port = address.port\n        resolve()\n      } else {\n        reject(new Error('Failed to start preview server'))\n      }\n    })\n  })\n\n  if (port === null) throw new Error('Failed to start preview server')\n  return port\n}\n\nexport async function createSandboxHtmlPreviewUrl(\n  filePath: string\n): Promise<{ success: boolean; url?: string; error?: string }> {\n  try {\n    const sandboxRoots = getSandboxRoots()\n    const fileStat = await lstat(filePath)\n    if (fileStat.isSymbolicLink()) {\n      return { success: false, error: 'Access denied: symlinks not allowed' }\n    }\n    const resolvedPath = await realpath(filePath)\n    const sandboxRoot = sandboxRoots.find((root) => isInside(root, resolvedPath))\n    if (!sandboxRoot) {\n      return { success: false, error: 'Access denied: path outside sandbox directory' }\n    }\n    if (!['.html', '.htm'].includes(path.extname(resolvedPath).toLowerCase())) {","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/chatboxai/chatbox/blob/81571269addb6bafb589a920b2883f1e1e084fd1/src/main/sandbox/preview-server.ts#L155-L191","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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'.","Retry start() a few times with backoff to absorb transient bind races when port 0 collides.","In sandboxed runtimes, confirm 127.0.0.1 is available; if not, the preview feature must be disabled for that environment.","Ensure the server instance is freshly created per start attempt (a closed server will not emit listen)."],"exampleFix":"// before\nserver?.once('error', reject)\nserver?.listen(0, '127.0.0.1', () => { ... else reject(new Error('Failed to start preview server')) })\n\n// after: include the underlying error cause\nserver?.once('error', (err) => reject(new Error('Failed to start preview server', { cause: err })))","handlingStrategy":"retry","validationCode":"if (server.listening) throw new Error('Preview server already started')","typeGuard":"function isPreviewStartFailure(e: unknown): e is Error { return e instanceof Error && e.message === 'Failed to start preview server' }","tryCatchPattern":"for (let attempt = 0; attempt < 3; attempt++) {\n  try { return await startPreviewServer() } catch (e) { if (!isPreviewStartFailure(e) || attempt === 2) throw e; await delay(200 * 2 ** attempt) }\n}","preventionTips":["Create a fresh http.Server per start attempt; a closed server will not emit listen.","In sandboxed runtimes, verify 127.0.0.1 availability before enabling previews.","Attach the 'error' listener before listen so EADDRINUSE surfaces with a cause."],"tags":["sandbox","preview-server","http","network","typescript"],"backgroundTag":null,"analyzedSha":"81571269addb6bafb589a920b2883f1e1e084fd1","analyzedAt":"2026-08-12T21:51:44.981Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}