Skip to content

ws: WebSocketServer.clients is undefined (should be a Set) — heartbeat loops kill the process #9325

Description

@proggeramlug

Summary

WebSocketServer.clients is undefined under Perry, where node has a
Set. Any for…of over it throws TypeError: is not iterable, and because
the idiomatic place to iterate it is a heartbeat setInterval, the throw lands
in a timer callback where application code cannot catch it — the process exits
and, under systemd, restart-loops with no traffic at all.

Perry 0.5.1519 from source plus the fixes in #9314 and #9319. ws 8.x,
Linux x86_64.

Repro

import { WebSocketServer } from "ws";

const wss = new WebSocketServer({ port: 0 });
console.log("clients  =", Object.prototype.toString.call(wss.clients));
console.log("iterator =", typeof (wss.clients as any)?.[Symbol.iterator]);
try {
  let n = 0;
  for (const _c of wss.clients) n += 1;
  console.log("iterated", n, "— OK");
} catch (e) {
  console.log("THREW:", String(e));
}
node 26.8.1 perry 0.5.1519
wss.clients [object Set] [object Undefined]
Symbol.iterator function undefined
for…of iterates (0) TypeError: is not iterable

The server object is otherwise constructed — the constructor returns, and
other members work; only clients is missing.

How it presents in a real service

This is the standard ws heartbeat, straight from the library's own docs:

const heartbeat = setInterval(() => {
  for (const ws of wss.clients) {        // ← throws here
    if (!alive.has(ws)) { ws.terminate(); continue; }
    alive.delete(ws);
    ws.ping();
  }
}, heartbeatMs);

Our API dies every few seconds on this, with no requests involved. It took
a gdb backtrace to find, because the JS-level message is only
TypeError: is not iterable at <anonymous> even with --debug-symbols:

#0  perry_runtime::symbol::iterator::throw_value_not_iterable ()
#1  js_get_iterator ()
#2  perry_closure_apps_api_src_ws_attach_ts ()
#3  js_interval_timer_tick ()
#4  perry_runtime::promise::microtasks::run_microtasks ()
#5  main ()

Why it is worth prioritising

  • Uncatchable. It throws inside a timer callback in library-idiomatic
    code, so no try/catch in application code prevents the exit.
  • No traffic needed. A service with zero connections still dies on the
    heartbeat, so it looks like a startup or config problem rather than a
    missing property.
  • wss.clients is how every ws server does connection tracking, heartbeats
    and broadcast — it is not a corner of the API.

A clear TypeError on access would already be a large improvement over an
undefined that only fails later, at a distance, inside a timer.

Activity

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions