Skip to content

Server process exits on connect timeout: ws.terminate() on a CONNECTING socket -> uncaughtException -> process.exit(1) #153

Description

@LootGoblin

Summary

Whenever the Unity WebSocket handshake fails to complete within connectTimeout (5s), the Node server process exits, closing the stdio pipe. From the MCP client's side the entire tool surface disappears mid-session (MCP error -32000: Connection closed).

The package already implements reconnection with exponential backoff — it never gets a chance to run, because the process is dead before the first retry.

Reproduced on 1.2.0 and 1.3.0 (c35f184) — Node v24.16.0, Windows 11, Unity 6.

Root cause

UnityConnection.doConnect() sets a connect-timeout timer:

this.connectionTimeoutTimer = setTimeout(() => {
  if (this.ws && this.ws.readyState === WebSocket.CONNECTING) {
    this.closeWebSocket('Connection timeout');
    ...
  }
}, this.config.connectTimeout);

closeWebSocket() clears the error handler, then terminates:

socket.onerror = null;        // <-- removes the only 'error' listener
...
try {
  socket.terminate();         // <-- socket is still CONNECTING
} catch (err) { ... }

ws.terminate() on a CONNECTING socket calls abortHandshake(), which emits 'error'. Because onerror was just cleared there is no listener, so Node's EventEmitter throws. The throw originates in a timer tick, so the surrounding try/catch cannot catch it:

Error: WebSocket was closed before the connection was established
    at WebSocket.terminate (node_modules/ws/lib/websocket.js:489:7)
    at UnityConnection.closeWebSocket (build/unity/unityConnection.js:396:20)
    at Timeout.<anonymous> (build/unity/unityConnection.js:174:26)
    at listOnTimeout (node:internal/timers:605:17)

It surfaces as an uncaughtException, and index.ts treats anything that isn't EPIPE/EOF/ERR_USE_AFTER_CLOSE as fatal:

serverLogger.error('Uncaught exception', error);
process.exit(1);

Why it's hard to spot

utils/logger.ts discards all output unless LOGGING=true or LOGGING_FILE=true. With neither set — the default when an MCP client launches the server over stdio — the crash reason goes to a no-op logger and the process dies completely silently. I only got the stack by re-running with LOGGING_FILE=true plus my own uncaughtException handler.

Reproduction

  1. Put the Unity bridge in a state where it accepts TCP but doesn't complete the handshake — e.g. a busy editor, or the wedged-listener state in Failed to start WebSocket server: Port <PORT NUMBER> is already in use #141.
  2. node Server~/build/index.js
  3. Exits with code 1 after ~5s, printing nothing.

In normal use the trigger is routine: any domain reload that outlasts the 5s connect timeout.

Suggested fix

Keep an error listener attached so abortHandshake's emit has somewhere to land:

socket.onopen = null;
socket.onmessage = null;
socket.onclose = null;
socket.removeAllListeners('pong');
socket.onerror = () => {};   // terminate() on a CONNECTING socket emits 'error';
                             // an EventEmitter with no 'error' listener throws.
try { socket.terminate(); } catch (err) { ... }

Worth considering alongside it: a fault originating in the Unity connection is recoverable by design, given the reconnect machinery already present. Treating every such fault as fatal turns a transient editor hiccup into a lost session. Downgrading connection-class faults to a warning would make the existing backoff logic actually reachable.

Workaround

A small launcher that imports the server and then replaces its process-level handlers with tolerant ones. With that in place, a single server process survived two bridge outages (including the #141 wedge) and three Unity restarts in one session, reconnecting each time, with no loss of tools.

Related: #141 — that issue is the Unity-side cause of the timeouts; this issue is about the client-side reaction to them being fatal.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions