Skip to content

Windows: browse server fails to start — findPort() race condition in Node.js polyfill #486

@nikobenho

Description

@nikobenho

Summary

On Windows, browse goto <url> fails 100% of the time with "Server failed to start within 15s". The root cause is a race condition in findPort() when running under the Node.js polyfill (bun-polyfill.cjs).

Environment

  • Windows 11 Pro
  • gstack v0.11.18.2
  • Node.js v20.11.1
  • Bun 1.3.11

Root cause

findPort() in server.ts tests port availability by calling Bun.serve({ port }) then immediately testServer.stop(). In real Bun, Bun.serve() is synchronous — the port binds and releases instantly.

In the Node.js polyfill, Bun.serve() wraps http.createServer() + server.listen(), which is async. stop() wraps server.close(), also async. Neither has completed by the time findPort() returns the port.

The timeline:

  1. findPort() calls polyfill Bun.serve({ port: X }) → queues async listen()
  2. testServer.stop() → queues async close()
  3. findPort() returns port X (neither listen nor close has executed yet)
  4. await browserManager.launch() — event loop runs, test server's listen() now binds port X
  5. Real Bun.serve({ port: X })EADDRINUSE because the test server's close() hasn't completed

Reproduction

Minimal script proving the race condition:

// Save as test-race.js, run with: node test-race.js
const path = require('path');
const polyfillPath = path.join(
  process.env.USERPROFILE, '.claude', 'skills', 'gstack', 'browse', 'dist', 'bun-polyfill.cjs'
);
require(polyfillPath);

async function test() {
  const port = 10000 + Math.floor(Math.random() * 50000);
  // Simulate findPort():
  const testServer = Bun.serve({ port, fetch: () => new Response('ok') });
  testServer.stop();
  // Simulate browserManager.launch() delay:
  await new Promise(r => setTimeout(r, 500));
  // Simulate real server bind:
  const http = require('http');
  const s = http.createServer((req, res) => res.end('ok'));
  s.on('error', (e) => {
    console.log('FAILED:', e.message); // Always hits EADDRINUSE
    process.exit(1);
  });
  s.listen(port, '127.0.0.1', () => {
    console.log('SUCCESS');
    s.close();
  });
}
test();

This fails 100% of the time on Windows.

Fix

Replace Bun.serve() port testing in findPort() with net.createServer() using proper async bind/close:

async function findPort() {
  const net = require('net');
  function checkPort(port) {
    return new Promise((resolve, reject) => {
      const srv = net.createServer();
      srv.once('error', (err) => reject(err));
      srv.listen(port, '127.0.0.1', () => {
        srv.close(() => resolve(port));
      });
    });
  }
  // ... same retry logic, using await checkPort(port) instead of Bun.serve()
}

This properly waits for both bind AND close to complete before returning the port.

Verified locally — 5/5 cold starts succeed, browse commands work normally.

Related issues

This is distinct from the stdio array fix in v0.11.18.2 — that fixed the spawn format, this fixes the port selection race condition.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions