Skip to content

fix: keep WebSocket binaryType + close handling stable across compatibility dates#405

Merged
threepointone merged 1 commit into
mainfrom
fix/websocket-binarytype-teardown
Jun 13, 2026
Merged

fix: keep WebSocket binaryType + close handling stable across compatibility dates#405
threepointone merged 1 commit into
mainfrom
fix/websocket-binarytype-teardown

Conversation

@threepointone

Copy link
Copy Markdown
Collaborator

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/voice when bumping compatibility dates: binary frames silently changed type, and clean closes started emitting spurious error logs.

The fixes

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) has always relied on ArrayBuffer, so it's 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 (e.g. when a DO is reset while a connection is open).

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

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 is ArrayBuffer, C2 single clean reciprocal close, C3 no unhandled rejections — for both hibernate modes. Reproduced the Blob regression on a real server at >= 2026-03-17, then confirmed all green after the fix across 2026-01-28 → 2026-06-11.

Layer B — vitest compat-matrix (CI gate). New BinaryTypeProbe DO + compat-binarytype.test.ts run via a parametrized vitest.compat.config.ts (compat date via COMPAT_DATE env). test:compat-matrix sweeps 2026-01-28 / 03-24 / 04-07 / 04-21 and is wired into PR CI. The gate is proven non-vacuous: reports blob without the pin, arraybuffer with it.

Also:

  • Bumped wrangler ^4.86.0 -> ^4.100.0 so local wrangler dev can run compat dates through 2026-06-11 (the older bundled workerd capped at 2026-05-03).
  • 6 new transport-errors.test.ts unit tests for the suppression predicate.
  • Full suite: 86 unit tests pass; typecheck, lint, format clean.

Changeset: patch -> 0.5.7.

Coordinated release

Cloudflare Agents pins partyserver and carries a matching change; the agents repo bumps its pin to ^0.5.7 once this publishes. Agents' own binaryType pin becomes defense-in-depth after this lands.

Test plan

  • npm run check (sherif + format + lint + type + 86 tests)
  • npm run test:compat-matrix -w partyserver green at all four dates
  • Real wrangler dev harness green 2026-01-28 → 2026-06-11 (in-memory + hibernating)
  • Verified gate fails (reports blob) when the pin is removed
  • CI green on this PR

Made with Cursor

…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-bot

changeset-bot Bot commented Jun 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3a46a69

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
partyserver Patch

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

@pkg-pr-new

pkg-pr-new Bot commented Jun 13, 2026

Copy link
Copy Markdown

Open in StackBlitz

hono-party

npm i https://pkg.pr.new/cloudflare/partykit/hono-party@405

partyfn

npm i https://pkg.pr.new/cloudflare/partykit/partyfn@405

partyserver

npm i https://pkg.pr.new/cloudflare/partykit/partyserver@405

partysocket

npm i https://pkg.pr.new/cloudflare/partykit/partysocket@405

partysub

npm i https://pkg.pr.new/cloudflare/partykit/partysub@405

partysync

npm i https://pkg.pr.new/cloudflare/partykit/partysync@405

partytracks

npm i https://pkg.pr.new/cloudflare/partykit/partytracks@405

partywhen

npm i https://pkg.pr.new/cloudflare/partykit/partywhen@405

y-partyserver

npm i https://pkg.pr.new/cloudflare/partykit/y-partyserver@405

commit: 3a46a69

@threepointone threepointone merged commit 7dbf92c into main Jun 13, 2026
6 checks passed
@threepointone threepointone deleted the fix/websocket-binarytype-teardown branch June 13, 2026 20:42
@github-actions github-actions Bot mentioned this pull request Jun 13, 2026
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.

1 participant