fix(worker): type-clean frame handling; name a Telegram close before any reply (#67)

Cloudflare's dashboard editor type-checks the script and underlined four
lines of the Blob handling, because a separate `blob` flag did not narrow
`event.data` (#66). binaryFrame() narrows each branch with instanceof; the
file now type-checks cleanly against current @cloudflare/workers-types.

When Telegram closes TCP without sending a byte, the Worker used to end the
WebSocket with a plain 1000. It now closes with 1011 "Telegram closed before
replying" and logs the destination and the bytes the client sent, so the
client log and the Cloudflare log both say where the tunnel broke (#53).

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-24 12:03:26 +00:00
committed by GitHub
parent f82543e44e
commit 3aa6f413a3
2 changed files with 62 additions and 15 deletions
+16 -2
View File
@@ -20,7 +20,7 @@ async function fixture({ opened = Promise.resolve(), write, timeout = false } =
const closed = deferred();
const writes = [];
const events = new Map();
const state = { connects: 0, accepted: false, socketCloses: 0, sent: [], closes: [] };
const state = { connects: 0, accepted: false, socketCloses: 0, sent: [], closes: [], warnings: [] };
const server = {
accept() { state.accepted = true; state.binaryTypeOnAccept = this.binaryType; },
addEventListener(name, callback) { events.set(name, callback); },
@@ -41,7 +41,7 @@ async function fixture({ opened = Promise.resolve(), write, timeout = false } =
},
};
const context = vm.createContext({
URL, Uint8Array, ArrayBuffer, Blob, setTimeout: timeout ? (fn) => setTimeout(fn, 0) : setTimeout, clearTimeout,
URL, Uint8Array, ArrayBuffer, Blob, console: { warn: (line) => state.warnings.push(line) }, setTimeout: timeout ? (fn) => setTimeout(fn, 0) : setTimeout, clearTimeout,
Response: class {
constructor(body, options) { this.body = body; Object.assign(this, options); }
},
@@ -208,6 +208,20 @@ test("Telegram bytes reach WebSocket unchanged and EOF closes both directions",
assert.equal(f.state.socketCloses, 1);
});
test("Telegram closing TCP before any reply is named, not a silent 1000", async () => {
// A tunnel that never carried a byte from Telegram used to end as a plain
// 1000 close, which hid where it broke (#53). The reason also reaches the
// Worker log in the Cloudflare dashboard.
const f = await fixture();
await f.fetch();
f.message(new Uint8Array([1, 2, 3]).buffer);
await tick();
f.eof();
for (let i = 0; i < 5; i++) await tick();
assert.deepEqual(f.state.closes[0], { code: 1011, reason: "Telegram closed before replying" });
assert.match(f.state.warnings[0], /149\.154\.167\.51 closed TCP before replying \(client sent 3 bytes\)/);
});
test("CDN worker keeps the requested DC203 destination", async () => {
const f = await fixture();
await f.fetch("/apiws?dst=91.105.192.100&dc=203");