Found 2026-07-30 during agent-canvas's live-workspace delivery, against 0.6.0 (uncaught exceptions in a @cloudflare/vitest-pool-workers real-DO run; reproducible whenever a client subscribes then closes quickly). Verified still present on current main (post-#36, 2026-08-26): #send at src/server/mixin.ts:1082 calls ws.send(encoded) unconditionally — no readyState check, no try/catch.
Symptom
TypeError: Can't call WebSocket send() after close().
at #send → #handleSub → #dispatch → webSocketMessage
logged as uncaught exception; source = Uncaught (in promise). Fires when a client sends sub and the socket transitions to CLOSING/CLOSED before the server finishes streaming the snapshot — the normal shape of a client disposing, navigating away, a StrictMode teardown, or a forced reconnect drop.
Expected
A socket that has gone away receiving a snapshot it can no longer read is expected under normal client churn — the socket's own close handler already tears down its subs — and should not surface as an uncaught error. Guard the send: skip when readyState !== OPEN, and/or wrap ws.send in try/catch treating a post-close send as a benign no-op.
Impact
Low–medium. No state corruption (outbound only, not the write path), but noisy uncaught-exception spam for every client that disconnects mid-handshake, and in a test harness it pollutes output and risks being flagged as a failure.
Acceptance criteria
sub (or any frame) arriving on a socket that closes mid-response produces no uncaught exception; the send is a no-op.
- No behavior change on the live path — deltas/receipts to OPEN sockets unaffected; the broadcast path (
Broadcaster) covered too if it shares the same raw-send exposure.
- Test pins the abrupt-close-mid-snapshot shape (real DO, hibernatable socket) and would fail on today's unguarded
ws.send.
Found 2026-07-30 during agent-canvas's live-workspace delivery, against 0.6.0 (uncaught exceptions in a
@cloudflare/vitest-pool-workersreal-DO run; reproducible whenever a client subscribes then closes quickly). Verified still present on currentmain(post-#36, 2026-08-26):#sendatsrc/server/mixin.ts:1082callsws.send(encoded)unconditionally — noreadyStatecheck, no try/catch.Symptom
logged as
uncaught exception; source = Uncaught (in promise). Fires when a client sendssuband the socket transitions to CLOSING/CLOSED before the server finishes streaming the snapshot — the normal shape of a client disposing, navigating away, a StrictMode teardown, or a forced reconnect drop.Expected
A socket that has gone away receiving a snapshot it can no longer read is expected under normal client churn — the socket's own close handler already tears down its subs — and should not surface as an uncaught error. Guard the send: skip when
readyState !== OPEN, and/or wrapws.sendin try/catch treating a post-close send as a benign no-op.Impact
Low–medium. No state corruption (outbound only, not the write path), but noisy uncaught-exception spam for every client that disconnects mid-handshake, and in a test harness it pollutes output and risks being flagged as a failure.
Acceptance criteria
sub(or any frame) arriving on a socket that closes mid-response produces no uncaught exception; the send is a no-op.Broadcaster) covered too if it shares the same raw-send exposure.ws.send.