WebSocket client: throw on file- or S3-backed Blobs in send(), ping() and pong() instead of sending an empty frame
Closed
RO
1mo ago+155+155+012345678901234567890123456789-1-1-0123456789
Problem
- On the client
WebSocket,ws.send(Bun.file("payload.bin"))puts an empty binary frame on the wire and reports nothing: the server'smessagehandler receives a 0-byte message.ws.ping(...)andws.pong(...)do the same with an empty ping/pong, and S3 blobs (s3.file(...)) and slices ofBun.file()behave the same way. ServerWebSocket.send(Bun.file(...))throwssend cannot send a file- or S3-backed Blob synchronously; await blob.bytes() firstfor the same input (since #36032), so the two sides of the same API disagree, and the client side loses data silently.- Cause:
WebSocket::send(JSBlob*),ping(JSBlob*)andpong(JSBlob*)insrc/jsc/bindings/webcore/WebSocket.cppread the payload withBlob__getDataPtr/Blob__getSize(src/runtime/webcore/Blob.rs), which return the blob's in-memory view. A file- or S3-backed blob has no in-memory bytes, so that view is empty and the overloads take their "empty blob" branch, which deliberately sends a 0-byte frame. Nothing distinguishes "empty blob" from "bytes not in memory".
Fix
- Adds
Blob__needsAsyncRead(Blob.rs, declared inblob.h): true when the blob's store is file- or S3-backed, the same predicateServerWebSocket'sblob_payloaduses. - The three client Blob overloads throw a
TypeErrorwith the server's message (send/ping/pongprefix) when it is true. The check sits after the existing readyState checks, so CONNECTING still throwsInvalidStateErrorand CLOSING/CLOSED is still a silent no-op, exactly as for every other payload type (and asServerWebSocket, which returns 0 when closed before validating the blob). - Why throwing is the right fix: these send paths are synchronous, and making
send(Bun.file())actually deliver the bytes would need an ordered send queue in the client (every latersend/ping/pong/closehas to wait behind the read) plus async failure reporting for the 125-byte ping/pong limit. That is a feature; the bug here is silent data loss, and the merged server-side API already resolved the same question by refusing loudly with a message that says what to do instead. In-memory blobs (including empty ones) are unaffected; therequire("ws")client wrapper passes blobs straight through, so with a callback it now reports the error there instead of sending an empty frame. - Verified with
test/js/web/websocket/websocket-blob.test.ts: three new cases (Bun.file(),Bun.file().slice(),S3Client.file()) assert thatsend,pingandpongeach throw the expectedTypeErrorand that the server received nothing but the trailing sentinel message; a fourth case pins the readyState ordering. The three Blob cases fail on the current release (undefinedthrown,binary:0/ping:0/pong:0received) and pass with this change. - Also run:
test/js/web/websocket/websocket-client.test.ts(37 pass) and the Blob / publish tests intest/js/bun/websocket/websocket-server.test.ts(34 pass) against the debug build.
Background
Blobin Bun is backed by aStore, which is one ofBytes(in memory),File(Bun.file(): a path or fd, read on demand) orS3(an object key, fetched on demand).shared_view()returns the in-memory bytes and is empty for the last two;needs_to_read_file()/is_s3()identify them.S3Fileis aJSBlobsubclass on the C++ side, so it reaches the same Blob overloads.- The client
WebSocketis WebCore's C++ class; its methods returnExceptionOr<void>, and the bindings turn a returnedException { TypeError, message }into a thrown JSTypeError. That is how the existing 125-byteRangeErrorfor ping/pong payloads is produced, and this change follows the same pattern.