Skip to content

[Windows] browse fails to start: Chromium disconnects on launch, then resolveDisconnectCause crashes with browser?.process is not a function #2085

Closed

Description

@alexrogal

Environment

  • gstack: 1.58.4.0 (also reproduced on 1.58.1.0)
  • OS: Windows 11 Pro (10.0.26200), running via git-bash
  • Node: v24.15.0, Bun: 1.3.14
  • Playwright Chromium installed: chromium-1208 + chromium_headless_shell-1208

Symptom

Every browse command (goto, --version, --help, status, …) prints:

[browse] Starting server...
[browse] Server failed to start within 15s

and exits 0. The headless browser never becomes usable, so /qa and /qa-only
cannot run on this machine.

Root cause

The 15s timeout is misleading — the server DOES start and bind a port, then the
daemon crashes immediately. Running the server directly surfaces the real error:

$ node dist/server-node.mjs
[browse] Server running on http://127.0.0.1:27493/ (PID: 15352)
[browse] Tab closed (id=1, remaining=0)
[browse] FATAL unhandled rejection: browser?.process is not a function

Two layered problems:

  1. Primary (the real blocker): On Windows the Chromium instance disconnects
    immediately at startup — the initial tab closes right after launch
    ("Tab closed (id=1, remaining=0)"). Likely the known Bun→Node→Chromium
    sandbox issue referenced in src/browser-manager.ts (Windows: browse.exe fails to maintain Chromium session — page reverts to about:blank #276).

  2. Secondary (masks the cause + kills the daemon): the disconnect handler
    resolveDisconnectCause() at src/browser-manager.ts:94 calls
    browser?.process(). Playwright's Browser has no .process() method, so
    this throws an unhandled rejection that crashes the whole server. A recoverable
    disconnect becomes a fatal startup failure, and the launcher only reports the
    generic "failed to start within 15s".

Not a browser-binary issue

Reproduced identically with three different Chromium binaries:

  • bundled Playwright chromium-1208
  • bundled chromium_headless_shell-1208
  • system Google Chrome via GSTACK_CHROMIUM_PATH="C:\Program Files\Google\Chrome\Application\chrome.exe"

Same Tab closed → browser?.process is not a function crash every time.

Suggested fix

  • Guard the .process() call so a disconnect during startup produces a clear,
    diagnosable error instead of an unhandled rejection, e.g.:
    const proc = typeof browser?.process === 'function' ? browser.process() : null;

(Won't restore browsing on its own, but stops the daemon from dying silently
and surfaces the real Windows-launch failure.)

Investigate the immediate Chromium disconnect on Windows/git-bash (#276) — this is the underlying blocker.
Repro
Windows 11 + git-bash, gstack 1.58.4.0
~/.claude/skills/gstack/browse/dist/browse goto https://example.com
Observe "Server failed to start within 15s"
node ~/.claude/skills/gstack/browse/dist/server-node.mjs to see the real crash

Activity

  1. DavidCnoops commented on Jul 10, 2026

    @DavidCnoops

    Confirming the secondary problem (resolveDisconnectCause → browser?.process()) on macOS — it is not Windows-specific and fires on every headless disconnect, not just launch.

    Environment: macOS 26.3.1 arm64, gstack 1.58.4.0 (9fd03fa), Bun 1.3.9, chromium_headless_shell-1208.

    Captured 3× with daemon stderr redirected to a file (normal spawns are stdio: 'ignore', so this never surfaces in the field):

    [browse] Received SIGTERM (ignoring — use /stop or Ctrl+C for intentional shutdown)
    [browse] Tab closed (id=1, remaining=0)
    [browse] FATAL unhandled rejection: browser?.process is not a function. (In 'browser?.process()', 'browser?.process' is undefined)
    

    In my case the disconnect was triggered by the CLI's kill+restart path during QA (#2219), but the handler bug is independent of the trigger: Playwright's client Browser (from chromium.launch()) has no .process() method — that's a Puppeteer / Playwright-BrowserServer API. So resolveDisconnectCause (browse/src/browser-manager.ts:94) throws on the first line for every headless disconnect, the intended crash-vs-clean classification never runs, and the intended diagnostic ("Chromium process crashed or was killed" / "closed cleanly") is never printed — the daemon dies as an opaque FATAL unhandled rejection via the module-level handler in server.ts instead.

    Consequences beyond Windows startup:

    • Every disconnect exits via the unhandledRejection path (exit 1) instead of the designed clean/crash exit codes — supervisors reading exit codes (per the comment above resolveDisconnectCause) can't distinguish user-quit from crash.
    • The masking made a different bug (CLI killing busy daemons) look like spontaneous daemon crashes for weeks of sessions on my machine.

    Suggested fix shape: Playwright's client Browser can't provide the child process handle, so the exit-code inspection can't work as written for launch()-mode. Either guard (typeof (browser as any)?.process === 'function') and fall back to a heuristic (e.g. browser.isConnected(), or track an intentionalShutdown flag set by the daemon's own stop path and classify everything else as crash), or launch via chromium.launchServer() + connect() if the process handle is genuinely needed.

  2. jungkeunmoon commented on Aug 1, 2026

    @jungkeunmoon

    Reproduced this end-to-end on another Windows 11 machine (Windows 11 Home 10.0.26200, gstack 1.60.1.0, Bun 1.3.14, chromium_headless_shell-1208) and traced it past the resolveDisconnectCause masking down to the actual Windows disconnect cause, which turned out to be two separate, fixable bugs rather than the sandbox issue from #276.

    1. resolveDisconnectCause guard (same fix already suggested above)

    Confirmed the browser?.process() crash and applied the guard exactly as suggested:

    const proc = typeof browser?.process === 'function' ? browser.process() : null;

    This alone just stops the daemon from dying opaquely — with it in place, DEBUG=pw:browser on the real disconnect surfaces the actual Chromium stderr instead of swallowing it in an unhandled rejection.

    2. Real cause #1: GPU process killed by Chromium's own watchdog

    With the guard in place, the disconnect turned out to be a real Chromium-side crash, not a masked non-issue:

    [pid=NNNNN][err] ERROR:content\browser\gpu\gpu_process_host.cc:996 GPU process exited unexpectedly: exit_code=143
    [pid=NNNNN][err] WARNING:content\browser\gpu\gpu_process_host.cc:1438 The GPU process has crashed 1 time(s)
    

    exit_code=143 happens on a fixed ~14s cadence every launch — that's Chromium's GPU watchdog force-killing an unresponsive GPU process, not resource pressure (18 logical CPUs, 16GB RAM, no other load). --disable-gpu alone does not stop the GPU process from spawning on current Chromium (145.0.7632.6) — the process still spawns and still hangs. --in-process-gpu (runs GPU services inside the browser process instead of a separate watchdog-supervised one) avoids the hang entirely. Verified by launching chrome-headless-shell.exe directly with the exact Playwright arg list plus/minus each flag.

    3. Real cause #2: chromium-headless-shell's network service crashes even with GPU fixed

    With GPU fixed, the browser survived past 14s but then reliably died 15-30s in with:

    [pid=NNNNN][err] ERROR:content\browser\network_service_instance_impl.cc:610 Network service crashed or was terminated, restarting service.
    

    No Windows Error Reporting entry, no Defender detection, no third-party AV/EDR — the network service utility process just doesn't come back. Ruled out a background Cisco AnyConnect VPN service as a red herring (stopping it didn't change anything).

    The fix: force Playwright to launch the full chromium binary instead of chromium-headless-shell. getExecutableName() in chromium.js always picks chromium-headless-shell when headless: true and no channel is set — passing channel: 'chromium' overrides that. Same headless launch, same flags, full binary survives indefinitely; standalone chromium-headless-shell crashes every time within ~30s. Confirmed with a 35s isolated Playwright script (chromium.launch({ headless: true, channel: 'chromium' })) run 3x back to back with zero crashes, vs 3/3 crashes on stock chromium-headless-shell.

    Combined patch (browse/src/browser-manager.ts)

    // resolveDisconnectCause
    const proc = typeof browser?.process === 'function' ? browser.process() : null;
    
    // BrowserManager.launch(), before the extensionsDir check
    if (process.platform === 'win32') {
      launchArgs.push('--disable-gpu', '--in-process-gpu');
    }
    
    // chromium.launch(...) call
    ...(process.platform === 'win32' ? { channel: 'chromium' } : {}),

    With all three in place, browse goto <url>, text, snapshot, and repeated stop/relaunch cycles ran clean and stable on this machine (previously 0% success rate — every launch crashed within 30s).

    One caveat: channel: 'chromium' means Windows users now need the full chromium browser installed, not just chromium-headless-shell (roughly +65MB). Might be worth gating this behind an env var (e.g. GSTACK_CHROMIUM_FULL=1) rather than always-on for win32, in case some Windows environments don't hit the headless-shell network-service crash and would rather keep the smaller download — I don't have another Windows machine to check how widespread this is.

    Happy to open a PR with this if useful — let me know if you'd rather gate it behind an env var or just ship it as the win32 default.

  3. added a commit that references this issue on Aug 15, 2026
    008dd65
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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