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.
Summary
WebSocketServer.clientsisundefinedunder Perry, where node has aSet. Anyfor…ofover it throwsTypeError: is not iterable, and becausethe idiomatic place to iterate it is a heartbeat
setInterval, the throw landsin 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.1519from source plus the fixes in #9314 and #9319.ws8.x,Linux x86_64.
Repro
wss.clients[object Set][object Undefined]Symbol.iteratorfunctionundefinedfor…ofTypeError: is not iterableThe server object is otherwise constructed — the constructor returns, and
other members work; only
clientsis missing.How it presents in a real service
This is the standard
wsheartbeat, straight from the library's own docs:Our API dies every few seconds on this, with no requests involved. It took
a
gdbbacktrace to find, because the JS-level message is onlyTypeError: is not iterable at <anonymous>even with--debug-symbols:Why it is worth prioritising
code, so no
try/catchin application code prevents the exit.heartbeat, so it looks like a startup or config problem rather than a
missing property.
wss.clientsis how everywsserver does connection tracking, heartbeatsand broadcast — it is not a corner of the API.
A clear
TypeErroron access would already be a large improvement over anundefinedthat only fails later, at a distance, inside a timer.