fix(worker): туннели через Worker рвались с 1003 на новых compatibility date; 2.0.0-beta.16 (#63)

* worker: set binaryType = "arraybuffer" to fix 1003 "binary messages required"

Since compatibility date 2026-03-17 Cloudflare delivers binary WebSocket messages as Blob (websocket_standard_binary_type). The message handler accepts only ArrayBuffer and closes every tunnel with 1003. Setting binaryType before accept() restores ArrayBuffer delivery regardless of the compatibility date.

* fix(worker): accept Blob frames and name an outdated Worker in the log; 2.0.0-beta.16

Since compatibility date 2026-03-17 Cloudflare delivers binary WebSocket
frames as Blob unless binaryType is "arraybuffer", and the Worker closed
every tunnel with 1003 right after the handshake. wrangler.toml itself pins
2026-09-19, so every fresh deployment was broken (#58, #50, #53).

On top of #61 (binaryType before accept) the Worker now also relays Blob
frames in order, and the client log tells the user to redeploy the script
when a Worker closes with "1003 binary messages required".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Dmitry Sankevich <16187308+avost@users.noreply.github.com>
Co-authored-by: babin <Thartewerner536e@engineer.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Никита Sonic
2026-10-22 22:37:15 +00:00
committed by GitHub
parent 05ce9566a8
commit 9529df3fdf
10 changed files with 79 additions and 20 deletions
+17 -8
View File
@@ -85,6 +85,7 @@ export default {
}
const [client, server] = Object.values(new WebSocketPair());
server.binaryType = "arraybuffer";
server.accept();
const writer = upstream.writable.getWriter();
@@ -116,28 +117,36 @@ export default {
server.addEventListener("message", (event) => {
if (closed) return;
if (!(event.data instanceof ArrayBuffer) && !ArrayBuffer.isView(event.data)) {
const data = event.data;
// С compatibility date 2026-03-17 бинарные кадры приходят как Blob, если
// binaryType не "arraybuffer". Свойство выставлено выше; Blob всё равно
// принимается — на случай рантайма, который его не учитывает. Раньше такой
// кадр закрывал туннель кодом 1003 сразу после рукопожатия.
const blob = typeof Blob !== "undefined" && data instanceof Blob;
if (!blob && !(data instanceof ArrayBuffer) && !ArrayBuffer.isView(data)) {
shutdown(1003, "binary messages required");
return;
}
const chunk = event.data instanceof ArrayBuffer
? new Uint8Array(event.data)
: new Uint8Array(event.data.buffer, event.data.byteOffset, event.data.byteLength);
if (pendingBytes + chunk.byteLength > MAX_PENDING_BYTES || pendingMessages >= MAX_PENDING_MESSAGES) {
const chunk = blob ? null : data instanceof ArrayBuffer
? new Uint8Array(data)
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
const size = blob ? data.size : chunk.byteLength;
if (pendingBytes + size > MAX_PENDING_BYTES || pendingMessages >= MAX_PENDING_MESSAGES) {
shutdown(1009, "Telegram write queue full");
return;
}
pendingBytes += chunk.byteLength;
pendingBytes += size;
pendingMessages += 1;
pending = pending
.then(async () => {
if (closed) return;
const bytes = chunk || new Uint8Array(await data.arrayBuffer());
await writer.ready;
if (!closed) await writer.write(chunk);
if (!closed) await writer.write(bytes);
})
.catch(() => shutdown(1011, "Telegram write failed"))
.finally(() => {
pendingBytes -= chunk.byteLength;
pendingBytes -= size;
pendingMessages -= 1;
});
});
+23 -2
View File
@@ -22,7 +22,7 @@ async function fixture({ opened = Promise.resolve(), write, timeout = false } =
const events = new Map();
const state = { connects: 0, accepted: false, socketCloses: 0, sent: [], closes: [] };
const server = {
accept() { state.accepted = true; },
accept() { state.accepted = true; state.binaryTypeOnAccept = this.binaryType; },
addEventListener(name, callback) { events.set(name, callback); },
close(code, reason) { state.closes.push({ code, reason }); },
send(chunk) { state.sent.push(Array.from(chunk)); },
@@ -41,7 +41,7 @@ async function fixture({ opened = Promise.resolve(), write, timeout = false } =
},
};
const context = vm.createContext({
URL, Uint8Array, ArrayBuffer, setTimeout: timeout ? (fn) => setTimeout(fn, 0) : setTimeout, clearTimeout,
URL, Uint8Array, ArrayBuffer, Blob, setTimeout: timeout ? (fn) => setTimeout(fn, 0) : setTimeout, clearTimeout,
Response: class {
constructor(body, options) { this.body = body; Object.assign(this, options); }
},
@@ -133,6 +133,27 @@ test("serialize binary writes, preserve bytes and view offsets", async () => {
f.event("close");
});
test("binary frames arrive as ArrayBuffer on new compatibility dates", async () => {
// With compatibility date 2026-03-17+ Cloudflare delivers Blob unless
// binaryType is set before accept(); the Worker then closed every tunnel
// with 1003 right after the handshake (#61).
const f = await fixture();
await f.fetch();
assert.equal(f.state.binaryTypeOnAccept, "arraybuffer");
f.event("close");
});
test("Blob frames are still relayed in order, never closed as text", async () => {
const f = await fixture();
await f.fetch();
f.message(new Blob([new Uint8Array([7, 8])]));
f.message(new Uint8Array([9]).buffer);
for (let i = 0; i < 5; i++) await tick();
assert.deepEqual(f.writes, [[7, 8], [9]]);
assert.deepEqual(f.state.closes, []);
f.event("close");
});
test("overflow closes socket and discards queued writes", async () => {
const f = await fixture();
await f.fetch();