-
Notifications
You must be signed in to change notification settings - Fork 6k
Description
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:
findPort()calls polyfillBun.serve({ port: X })→ queues asynclisten()testServer.stop()→ queues asyncclose()findPort()returns port X (neither listen nor close has executed yet)await browserManager.launch()— event loop runs, test server'slisten()now binds port X- Real
Bun.serve({ port: X })→ EADDRINUSE because the test server'sclose()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
- Windows: browse.exe won't start (stdio bug) + cookie import completely non-functional #448, Windows: browse daemon fails with 'stdio must be an array' #454, browse.exe server spawn broken on Windows (Bun.spawn fails silently) (Permanent fix found) #458 — prior Windows browse fixes (stdio format, process detection)
- browse: Playwright Chromium launch hangs on Windows (Bun pipe transport issue) #77, browse.exe fails on Windows: --remote-debugging-pipe not working with Bun runtime #254 — earlier Bun/Windows pipe transport 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.