fix: keep WebSocket binaryType + close handling stable across compatibility dates#405
Merged
Merged
Conversation
…dates
Three related WebSocket fixes so PartyServer behaves identically on every
Worker compatibility date, plus the tooling to prove it. All changes are
no-ops on older dates and corrective on newer ones (backward compatible).
1. Pin binaryType to "arraybuffer" for non-hibernating connections.
On compat dates >= 2026-03-17 the `websocket_standard_binary_type` flag
flips the default server-side `binaryType` from "arraybuffer" to "blob",
so binary frames arrived as `Blob` instead of `ArrayBuffer` on the
in-memory accept path. Every PartyServer consumer (and frameworks built
on it, e.g. Cloudflare Agents / @cloudflare/voice) has always received
`ArrayBuffer`, so it is pinned back in `InMemoryConnectionManager.accept`.
The Hibernation API is unaffected (it always delivers `ArrayBuffer`).
2. Accept non-hibernating connections in half-open mode.
`accept({ allowHalfOpen: true })` (with a try/catch fallback to bare
accept() for older runtimes) keeps PartyServer's manual close handshake
in control on compat dates >= 2026-04-07, where `web_socket_auto_reply_to_close`
would otherwise auto-tear-down a tunneled DO socket and surface a spurious
retryable "Network connection lost." rejection.
3. Stop reporting transport-teardown errors via onError.
A retryable "Network connection lost." / "WebSocket peer disconnected"
error that fires on an already CLOSING/CLOSED connection is the socket
going away during the close handshake, not an application error. New
`transport-errors.ts` (`isBenignTeardownError`, structured `retryable`-first
detection so it stays correct under `enhanced-error-serialization` >=
2026-04-21) suppresses it in both the in-memory `handleErrorFromClient`
and hibernating `webSocketError` paths. Genuine mid-connection (OPEN)
errors still reach onError.
Verification:
- Bumped wrangler ^4.86.0 -> ^4.100.0 so local `wrangler dev` runs compat
dates through 2026-06-11 (older workerd capped at 2026-05-03).
- New `BinaryTypeProbe` DO + `compat-binarytype.test.ts` + parametrized
`vitest.compat.config.ts` lock the ArrayBuffer contract across dates; the
`test:compat-matrix` script runs it at 2026-01-28 / 03-24 / 04-07 / 04-21
and is wired into PR CI. The gate is non-vacuous (reports "blob" without
the pin, "arraybuffer" with it).
- 6 new `transport-errors.test.ts` unit tests cover the suppression predicate.
- Full suite: 86 unit tests pass; typecheck, lint, format clean.
Changeset: patch -> 0.5.7.
Co-authored-by: Cursor <cursoragent@cursor.com>
🦋 Changeset detectedLatest commit: 3a46a69 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
hono-party
partyfn
partyserver
partysocket
partysub
partysync
partytracks
partywhen
y-partyserver
commit: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three related WebSocket fixes so PartyServer behaves identically on every Worker
compatibility_date, plus the tooling to prove it. Each change is a no-op on older dates and corrective on newer ones — fully backward compatible for existing projects, not just the latest date.Motivated by downstream breakage in Cloudflare Agents /
@cloudflare/voicewhen bumping compatibility dates: binary frames silently changed type, and clean closes started emitting spurious error logs.The fixes
1. Pin
binaryTypeto"arraybuffer"for non-hibernating connectionsOn compat dates
>= 2026-03-17thewebsocket_standard_binary_typeflag flips the default server-sidebinaryTypefrom"arraybuffer"to"blob", so binary frames arrived asBlobinstead ofArrayBufferon the in-memory accept path. Every PartyServer consumer (and frameworks built on it) has always relied onArrayBuffer, so it's pinned back inInMemoryConnectionManager.accept. The Hibernation API is unaffected (it always deliversArrayBuffer).2. Accept non-hibernating connections in half-open mode
accept({ allowHalfOpen: true })(with atry/catchfallback to bareaccept()for older runtimes) keeps PartyServer's manual close handshake in control on compat dates>= 2026-04-07, whereweb_socket_auto_reply_to_closewould otherwise auto-tear-down a tunneled DO socket and surface a spurious retryableNetwork connection lost.rejection (e.g. when a DO is reset while a connection is open).3. Stop reporting transport-teardown errors via
onErrorA retryable
Network connection lost./WebSocket peer disconnectederror that fires on an alreadyCLOSING/CLOSEDconnection is the socket going away during the close handshake, not an application error. Newtransport-errors.ts(isBenignTeardownError, structuredretryable-first detection so it stays correct underenhanced-error-serialization>= 2026-04-21) suppresses it in both the in-memoryhandleErrorFromClientand hibernatingwebSocketErrorpaths. Genuine mid-connection (OPEN) errors still reachonError.Verification
Two independent layers, both real:
Layer A — real
wrangler dev+ real WebSocket client (temporary.compat-harness/, not committed). Spawns a real dev server per compat date and drives a real WS client through: C1 binaryType isArrayBuffer, C2 single clean reciprocal close, C3 no unhandled rejections — for both hibernate modes. Reproduced theBlobregression on a real server at>= 2026-03-17, then confirmed all green after the fix across2026-01-28 → 2026-06-11.Layer B — vitest compat-matrix (CI gate). New
BinaryTypeProbeDO +compat-binarytype.test.tsrun via a parametrizedvitest.compat.config.ts(compat date viaCOMPAT_DATEenv).test:compat-matrixsweeps2026-01-28 / 03-24 / 04-07 / 04-21and is wired into PR CI. The gate is proven non-vacuous: reportsblobwithout the pin,arraybufferwith it.Also:
^4.86.0 -> ^4.100.0so localwrangler devcan run compat dates through2026-06-11(the older bundled workerd capped at2026-05-03).transport-errors.test.tsunit tests for the suppression predicate.Changeset:
patch-> 0.5.7.Coordinated release
Cloudflare Agents pins
partyserverand carries a matching change; the agents repo bumps its pin to^0.5.7once this publishes. Agents' ownbinaryTypepin becomes defense-in-depth after this lands.Test plan
npm run check(sherif + format + lint + type + 86 tests)npm run test:compat-matrix -w partyservergreen at all four dateswrangler devharness green2026-01-28 → 2026-06-11(in-memory + hibernating)blob) when the pin is removedMade with Cursor