fix(websocket): add missing incPendingActivityCount() in blob binaryType case (#26670)

## Summary

- Fix crash ("Pure virtual function called!") when WebSocket client
receives binary data with `binaryType = "blob"` and no event listener
attached
- Add missing `incPendingActivityCount()` call before `postTask` in the
Blob case of `didReceiveBinaryData`
- Add regression test for issue #26669

## Root Cause

The Blob case in `didReceiveBinaryData` (WebSocket.cpp:1324-1331) was
calling `decPendingActivityCount()` inside the `postTask` callback
without a matching `incPendingActivityCount()` beforehand. This bug was
introduced in #21471 when Blob support was added.

The ArrayBuffer and NodeBuffer cases correctly call
`incPendingActivityCount()` before `postTask`, but the Blob case was
missing this call.

## Test plan

- [x] New regression test verifies WebSocket with `binaryType = "blob"`
doesn't crash on ping frames
- [x] `bun bd test test/regression/issue/26669.test.ts` passes

Fixes #26669

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: Ciro Spaciari MacBook <ciro@anthropic.com>
This commit is contained in:
robobun
2026-02-05 20:39:19 -08:00
committed by GitHub
parent 2019a1b11d
commit 89d2b1cd0b
2 changed files with 70 additions and 0 deletions

View File

@@ -1323,6 +1323,7 @@ void WebSocket::didReceiveBinaryData(const AtomString& eventName, const std::spa
if (auto* context = scriptExecutionContext()) {
RefPtr<Blob> blob = Blob::create(binaryData, context->jsGlobalObject());
this->incPendingActivityCount();
context->postTask([this, name = eventName, blob = blob.releaseNonNull(), protectedThis = Ref { *this }](ScriptExecutionContext& context) {
ASSERT(scriptExecutionContext());
protectedThis->dispatchEvent(MessageEvent::create(name, blob, protectedThis->m_url.string()));

View File

@@ -0,0 +1,69 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// https://github.com/oven-sh/bun/issues/26669
// WebSocket client crashes ("Pure virtual function called!") when binaryType = "blob"
// and no event listener is attached. The missing incPendingActivityCount() allows the
// WebSocket to be GC'd before the postTask callback runs.
test("WebSocket with binaryType blob should not crash when GC'd before postTask", async () => {
await using server = Bun.serve({
port: 0,
fetch(req, server) {
if (server.upgrade(req)) return undefined;
return new Response("Not a websocket");
},
websocket: {
open(ws) {
// Send binary data immediately - this triggers didReceiveBinaryData
// with the Blob path when client has binaryType = "blob"
ws.sendBinary(new Uint8Array(64));
ws.sendBinary(new Uint8Array(64));
ws.sendBinary(new Uint8Array(64));
},
message() {},
},
});
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const url = process.argv[1];
// Create many short-lived WebSocket objects with blob binaryType and no listeners.
// Without the fix, the missing incPendingActivityCount() lets the WebSocket get GC'd
// before the postTask callback fires, causing "Pure virtual function called!".
async function run() {
for (let i = 0; i < 100; i++) {
const ws = new WebSocket(url);
ws.binaryType = "blob";
// Intentionally: NO event listeners attached.
// This forces the postTask path in didReceiveBinaryData's Blob case.
}
// Force GC to collect the unreferenced WebSocket objects while postTask
// callbacks are still pending.
Bun.gc(true);
await Bun.sleep(50);
Bun.gc(true);
await Bun.sleep(50);
Bun.gc(true);
await Bun.sleep(100);
}
await run();
Bun.gc(true);
await Bun.sleep(200);
console.log("OK");
process.exit(0);
`,
`ws://localhost:${server.port}`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("OK");
expect(exitCode).toBe(0);
});