Skip to content

Report web socket connection failures instead of hanging on "Connecting …" - #193

Merged
binarykitchen merged 1 commit into
masterfrom
fix/report-websocket-connection-failures
Sep 8, 2026
Merged

Report web socket connection failures instead of hanging on "Connecting …"#193
binarykitchen merged 1 commit into
masterfrom
fix/report-websocket-connection-failures

Conversation

@binarykitchen

Copy link
Copy Markdown
Owner

Problem

When the web socket never reaches OPEN, the recorder stays on Connecting … forever. No ERROR is emitted, nothing is logged above debug level, and the host application has no way to react or tell the user anything.

This is trivially reproducible: point socketUrl at a hostname that does not resolve, or at a host that refuses the connection. I hit it running the Videomail app in an Android emulator, where the configured local dev domain only resolves on the host machine.

Cause

Three separate things line up to swallow the failure.

1. The close handler expected an error argument that never arrives.

this.stream.on("close", (err) => {
  this.connecting = this.connected = false;

  if (err) {
    this.emit("ERROR", { err });
  } else if (this.userMediaLoaded) {
    this.initSocket();
  }
});

Node streams (and duplexify, which websocket-stream builds on) never pass an argument to close. So err is always undefined and the whole ERROR branch is dead code.

2. The fallback branch cannot fire during the initial connect.

The else if only reconnects when userMediaLoaded is true. During the very first connect it never is, because the camera is not loaded until the socket is up. So a socket that dies before connecting hits neither branch and the recorder simply goes quiet.

The browser does give us a signal here — a failed handshake fires error and then close with code 1006 — but websocket-stream routes error into stream.destroy(err), and our error handler only writes a debug line.

3. options.timeouts.connection was never applied to the web socket.

It is documented as "in seconds, increase if api is slow" and honoured in resource.ts for HTTP, but the web socket connect had no timeout at all. A stalled connect (packets silently dropped rather than refused) fires neither error nor close until the OS gives up, which can take minutes.

Fix

  • close no longer takes a phantom error argument. Instead it records whether the socket ever connected, and reports a failure when it did not. Reconnecting after an established connection drops is unchanged.
  • options.timeouts.connection now also guards the web socket connect, covering the stalled case.
  • Both paths funnel into one failConnection() method, guarded so a single attempt emits at most one ERROR, and so it destroys the pending stream rather than leaving it dangling.
  • unload() clears the connection timeout and drops connecting before destroying the stream, so tearing down a pending connection is not misreported as a failure.

The emitted error names the socketUrl that was attempted, which makes misconfiguration self-explanatory:

Unable to connect to the server
The connection to wss://…/ws was refused or could not be reached. Please check your internet connection and try again. If the problem persists, contact us.

Notes

  • No behaviour change on the happy path, nor for reconnects after a working connection drops.
  • No new options. This makes an existing, already documented option actually do what it says for web sockets.
  • No unit test: Recorder has no test harness in this repo (it needs Visuals, Replay and a live DOM), and standing one up is well beyond this fix. Verified manually against an unresolvable host, a refused port and a healthy connection.
  • npm run types, npm run lint, npm run prettier and npm test all pass.

When the web socket never reaches OPEN the recorder stayed on
"Connecting …" indefinitely, with no error and no way for the host
application to react.

Three causes:

- The stream "close" handler expected an error argument, but Node
  streams never pass one on close, so that branch was dead code.
- A socket that closed without ever having connected only triggered a
  reconnect when user media was already loaded. During the initial
  connect it never is, so nothing happened at all.
- options.timeouts.connection was only honoured for HTTP requests, so a
  stalled connect had no timeout of its own.

Now a socket that closes before connecting emits ERROR naming the
socketUrl, and options.timeouts.connection also guards the web socket
connect for the case where the connection stalls rather than being
refused. Reconnecting after an established connection drops is
unchanged.
@binarykitchen
binarykitchen merged commit 3d68ba1 into master Sep 8, 2026
9 checks passed
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.

1 participant