Skip to content

Keep setTypeOfService EINVAL from crashing the daemon and server - #4627

Merged
ymichael merged 1 commit into
mainfrom
bb/high-priority-host-daemon-crashes-daily-on-macos-thr_nzeqgckxvy
Oct 1, 2026
Merged

ymichael merged 1 commit into
mainfrom
bb/high-priority-host-daemon-crashes-daily-on-macos-thr_nzeqgckxvy

Conversation

@ymichael

@ymichael ymichael commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Human comments

What was wrong

Node 24.15+ added net.Socket#setTypeOfService, and the undici 7.x it bundles calls socket.setTypeOfService(0) from writeH1 on every new socket's first request without a try/catch. Node's net throws on any setsockopt failure except on Windows. On macOS, setsockopt returns EINVAL once the peer has reset the connection, so one fetch whose connection is reset before the request is written throws from the socket connect callback as an uncaughtException. The fetch promise is never rejected, and the whole host daemon or server exits, interrupting every running turn on the host.

Upstream fixed this in nodejs/undici#5547 (released in undici 8.8.0+, bundled in Node 26.5.1+), but it was never backported to undici 7.x. Every Node 24 release through 24.21.0 is affected, and every Electron line through the 46 nightlies still ships Node 24. bb 0.44's Electron 44.3.0 has Node 24.20.0 / undici 7.29.0. Bumping Electron cannot fix it. Node 22 has no setTypeOfService, so dev machines on nvm Node 22 never see it.

What changed

  • packages/process-utils: new installSocketTypeOfServiceGuard() wraps net.Socket.prototype.setTypeOfService and ignores only EINVAL. The request then continues, fails writing to the reset socket, and rejects its own promise with fetch failed. On runtimes without the API (Node 22) it does nothing, and it becomes inert once undici stops making the call by default.
  • apps/host-daemon/src/index.ts and apps/server/src/index.ts install it next to installSafeProcessDiagnostics. The server was exposed too (macOS BB 0.43.4: recurrent host-daemon and server exits from uncaught Node/Undici setTypeOfService EINVAL #4280 recorded 6 server crashes with the same stack); this also covers the in-process plugins that bundle npm undici 7.28 (account-pool, push-notifications).
  • No wire changes, so HOST_DAEMON_PROTOCOL_VERSION is unchanged.

Alternatives considered: no fixed Node 24 or Electron release exists to bump to. Routing all fetches through an npm undici v8 dispatcher also prevented the crash in testing, but it would move every fetch in the process to a different undici major, add a runtime dependency, and not cover plugins that bundle their own undici.

Not covered: plugin host workers and provider bridges run as separate Node processes and don't install the guard. A crash there restarts that worker, not the daemon.

How you verified

  • Deterministic repro without bb: a TCP server that resets every connection on accept, plus a fetch that stalls the event loop for 300 ms. On Electron 41.7.0 (Node 24.15.0) and Electron 44.3.0 (Node 24.20.0) it threw an uncaughtException 10/10 times, with the reporter's exact stack (net:911, undici:8022/7758/9656/9589/9370/3342). Even with no stall it crashed 50–75% of runs. On Node 22 the same fetch rejects with EPIPE. With the guard, 20/20 runs rejected with fetch failed.
  • Real daemon: the daemon bundle from this branch ran on Electron 44.3.0's Node 24.20.0, pointed at a TCP proxy that can switch to resetting every new connection.
    • Without the guard it crashed twice (after 25 s and 5 s) with the reporter's stack and wrote process-host-daemon-uncaughtException-*.json (v24.20.0).
    • With the guard it stayed up through 2 minutes of resets (20 resets, 14 rejected session-open fetches) and the same process reconnected afterwards.
  • New test packages/process-utils/test/socket-type-of-service.test.ts:
    • The error-code filtering test runs everywhere.
    • The real resetting-peer test runs only on runtimes with setTypeOfService, so it is skipped in CI (Node 22). On Node 24.20 it passes with the guard and fails with the guard removed (Unhandled Error: setTypeOfService EINVAL).
  • pnpm exec turbo run typecheck lint --filter=@bb/process-utils --filter=@bb/host-daemon --filter=@bb/server and turbo run test for @bb/process-utils and @bb/host-daemon pass.

Fixes #4280

🤖 Generated with Claude Code

AGENT GENERATED

Node 24.15+ bundles undici 7.x, whose HTTP/1.1 writer calls
socket.setTypeOfService() on each new socket's first request without a
try/catch, and Node's net throws on any non-Windows setsockopt failure.
On macOS setsockopt returns EINVAL once the peer has reset the
connection, so one fetch whose connection is reset before the request is
written throws from the socket connect callback as an uncaught exception
and exits the whole process instead of rejecting that fetch.

undici fixed this in v8.8.0 (nodejs/undici#5547), but it was never
backported to v7.x. Every Node 24 release through 24.21.0 and every
Electron line through 46 nightly still ship the unguarded call, so
bumping Electron cannot fix it. Install a narrow guard in the host daemon
and server entrypoints that ignores EINVAL from setTypeOfService; the
request then continues, fails writing to the reset socket, and rejects
its own promise. The guard is a no-op on runtimes without the API (Node
22) and becomes inert once undici stops calling it by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymichael
ymichael merged commit ac0a44d into main Oct 1, 2026
31 of 33 checks passed
@ymichael
ymichael deleted the bb/high-priority-host-daemon-crashes-daily-on-macos-thr_nzeqgckxvy branch October 1, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

macOS BB 0.43.4: recurrent host-daemon and server exits from uncaught Node/Undici setTypeOfService EINVAL

1 participant