Skip to content

fix(server): guard #send against closed sockets — no uncaught throw on abrupt sub - #41

Merged
grrowl merged 1 commit into
mainfrom
fix/server-send-guard
Aug 26, 2026
Merged

fix(server): guard #send against closed sockets — no uncaught throw on abrupt sub#41
grrowl merged 1 commit into
mainfrom
fix/server-send-guard

Conversation

@grrowl

@grrowl grrowl commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Fixes #40

Problem

#send (src/server/mixin.ts) called ws.send(encoded) unconditionally — no readyState check, no try/catch. When a client sends sub and the socket transitions to CLOSING/CLOSED before the snapshot finishes streaming (normal churn: dispose, navigate-away, StrictMode teardown, forced reconnect drop), ws.send() throws Can't call WebSocket send() after close(), surfacing as an uncaught exception.

Fix

Guard #send: skip when readyState !== WebSocket.OPEN, and wrap ws.send in try/catch to cover the OPEN→closed race the pre-check can't. A post-close send is treated as a benign no-op — the socket's webSocketClose already tears down its subs (#dropSocketSubs), so no additional cleanup is needed (stated in a code comment).

  • Outbound-only; no state impact, no behavior change for OPEN sockets.
  • The Broadcaster egress path is covered too — it routes every send through the same #send closure (new Broadcaster((ws, frame) => this.#send(ws, frame), …)), the single raw-send chokepoint.

Test

New test in tests/ws-lifecycle.test.ts pins the abrupt-close-mid-snapshot shape against a real hibernatable DO socket: open the socket, close the server-side socket, then hand it the sub frame so the terminal snap-end #send lands post-close. It asserts webSocketMessage resolves rather than rejecting.

Verified to fail on unpatched main — before the fix it rejected with the exact issue symptom:

TypeError: Can't call WebSocket send() after close().
  at SyncTestDO.#send src/server/mixin.ts:1096
  at SyncTestDO.#handleSub src/server/mixin.ts:1048  (snap-end)

After the fix: passes.

Validation

  • npm test — full suite green (265 passed).
  • npm run typecheck — clean.

This is a bug fix within existing decisions (outbound-only, ADR-0002/0006 unaffected) — no ADR. CHANGELOG entry added under [Unreleased] → Fixed.

🤖 Generated with Claude Code

…n abrupt sub

A client that subscribes then closes before the snapshot finishes streaming
(dispose, navigate-away, StrictMode teardown, forced reconnect) drove `#send`
to call `ws.send()` on a CLOSING/CLOSED socket, surfacing `Can't call send()
after close()` as an uncaught exception. The socket's `webSocketClose` already
tears down its subs, so a frame it can no longer read is a benign no-op.

Skip sends on a non-OPEN socket and wrap `ws.send` in try/catch for the
OPEN→closed race the check can't cover. Outbound-only; no state impact, no
behavior change for OPEN sockets. The `Broadcaster` egress path is covered too
— it routes through the same `#send` closure.

Fixes #40

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@grrowl
grrowl merged commit 169bab6 into main Aug 26, 2026
1 check passed
@grrowl
grrowl deleted the fix/server-send-guard branch August 26, 2026 05:37
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.

server: #send has no readyState guard — sub whose socket closes mid-snapshot throws uncaught

1 participant