From e5158791c4fa3518eade7b0c71ebbc2dd25572a0 Mon Sep 17 00:00:00 2001 From: Inqnuam Date: Tue, 8 Apr 2025 02:10:27 +0200 Subject: [PATCH 1/2] fix(ws): allow blob to be set as WebSocket Client binaryType --- src/bun.js/bindings/webcore/WebSocket.cpp | 8 ++++---- src/js/thirdparty/ws.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/bun.js/bindings/webcore/WebSocket.cpp b/src/bun.js/bindings/webcore/WebSocket.cpp index 23d5116ebbb3..8d8c8b62039d 100644 --- a/src/bun.js/bindings/webcore/WebSocket.cpp +++ b/src/bun.js/bindings/webcore/WebSocket.cpp @@ -934,10 +934,10 @@ String WebSocket::binaryType() const ExceptionOr WebSocket::setBinaryType(const String& binaryType) { - // if (binaryType == "blob"_s) { - // m_binaryType = BinaryType::Blob; - // return {}; - // } + if (binaryType == "blob"_s) { + m_binaryType = BinaryType::Blob; + return {}; + } if (binaryType == "arraybuffer"_s) { m_binaryType = BinaryType::ArrayBuffer; return {}; diff --git a/src/js/thirdparty/ws.js b/src/js/thirdparty/ws.js index 64d1f9a31b4d..6301ca72e040 100644 --- a/src/js/thirdparty/ws.js +++ b/src/js/thirdparty/ws.js @@ -293,7 +293,7 @@ class BunWebSocket extends EventEmitter { } set binaryType(value) { - if (value === "nodebuffer" || value === "arraybuffer") { + if (value === "nodebuffer" || value === "arraybuffer" || value === "blob") { this.#ws.binaryType = this.#binaryType = value; this.#fragments = false; } else if (value === "fragments") { From 86e4b43385bb5c3e63eb611966b8937751a0bf53 Mon Sep 17 00:00:00 2001 From: Inqnuam Date: Thu, 10 Apr 2025 00:17:35 +0200 Subject: [PATCH 2/2] fix(ws): implement Blob binaryType on both client & server --- src/bun.js/bindings/webcore/WebSocket.cpp | 3 +- src/js/thirdparty/ws.js | 69 +++++++++++++---------- test/js/first_party/ws/ws.test.ts | 4 ++ 3 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/bun.js/bindings/webcore/WebSocket.cpp b/src/bun.js/bindings/webcore/WebSocket.cpp index 8d8c8b62039d..9b7252aeb9f9 100644 --- a/src/bun.js/bindings/webcore/WebSocket.cpp +++ b/src/bun.js/bindings/webcore/WebSocket.cpp @@ -935,7 +935,8 @@ String WebSocket::binaryType() const ExceptionOr WebSocket::setBinaryType(const String& binaryType) { if (binaryType == "blob"_s) { - m_binaryType = BinaryType::Blob; + // NOTE: this is a temporary solution + m_binaryType = BinaryType::NodeBuffer; return {}; } if (binaryType == "arraybuffer"_s) { diff --git a/src/js/thirdparty/ws.js b/src/js/thirdparty/ws.js index 6301ca72e040..fe349420199d 100644 --- a/src/js/thirdparty/ws.js +++ b/src/js/thirdparty/ws.js @@ -28,7 +28,7 @@ function emitWarning(type, message) { } // TODO: add private method on WebSocket to avoid these allocations -function normalizeData(data, opts) { +async function normalizeData(data, opts) { const isBinary = opts?.binary; if (typeof data === "number") { @@ -39,6 +39,8 @@ function normalizeData(data, opts) { data = Buffer.from(data); } else if (isBinary === false && $isTypedArrayView(data)) { data = new Buffer(data.buffer, data.byteOffset, data.byteLength).toString("utf-8"); + } else if (data instanceof Blob) { + data = await data.arrayBuffer(); } return data; @@ -200,7 +202,8 @@ class BunWebSocket extends EventEmitter { ({ data }) => { const isBinary = typeof data !== "string"; if (isBinary) { - this.emit("message", this.#fragments ? [data] : data, isBinary); + const formattedData = this.#binaryType === "blob" ? new Blob([data]) : data; + this.emit("message", this.#fragments ? [formattedData] : formattedData, isBinary); } else { let encoded = encoder.encode(data); if (this.#binaryType !== "arraybuffer") { @@ -231,7 +234,8 @@ class BunWebSocket extends EventEmitter { this.#ws.addEventListener( "pong", ({ data }) => { - this.emit("pong", data); + const formattedData = this.#binaryType === "blob" ? new Blob([data]) : data; + this.emit("pong", formattedData); }, once, ); @@ -254,16 +258,18 @@ class BunWebSocket extends EventEmitter { opts = undefined; } - try { - this.#ws.send(normalizeData(data, opts), opts?.compress); - } catch (error) { + normalizeData(data, opts).then(normalizedData => { + try { + this.#ws.send(normalizedData, opts?.compress); + } catch (error) { + // Node.js APIs expect callback arguments to be called after the current stack pops + typeof cb === "function" && process.nextTick(cb, error); + return; + } + // deviation: this should be called once the data is written, not immediately // Node.js APIs expect callback arguments to be called after the current stack pops - typeof cb === "function" && process.nextTick(cb, error); - return; - } - // deviation: this should be called once the data is written, not immediately - // Node.js APIs expect callback arguments to be called after the current stack pops - typeof cb === "function" && process.nextTick(cb, null); + typeof cb === "function" && process.nextTick(cb, null); + }); } close(code, reason) { @@ -848,26 +854,31 @@ class BunWebSocketMocked extends EventEmitter { } if (this.#state === 1) { - const compress = opts?.compress; - data = normalizeData(data, opts); - // send returns: - // 1+ - The number of bytes sent is always the byte length of the data never less - // 0 - dropped due to backpressure (not sent) - // -1 - enqueue the data internaly - // we dont need to do anything with the return value here - const written = this.#ws.send(data, compress); - if (written === 0) { - // dropped - this.#enquedMessages.push([data, compress, cb]); - this.#bufferedAmount += data.length; - return; - } + normalizeData(data, opts).then(normalizedData => { + const compress = opts?.compress; + + // send returns: + // 1+ - The number of bytes sent is always the byte length of the data never less + // 0 - dropped due to backpressure (not sent) + // -1 - enqueue the data internaly + // we dont need to do anything with the return value here + const written = this.#ws.send(normalizedData, compress, () => {}); + + if (written === 0) { + // dropped + this.#enquedMessages.push([normalizedData, compress, cb]); + this.#bufferedAmount += normalizedData.length; + return; + } - typeof cb === "function" && process.nextTick(cb); + typeof cb === "function" && process.nextTick(cb); + }); } else if (this.#state === 0) { // not connected yet - this.#enquedMessages.push([data, opts?.compress, cb]); - this.#bufferedAmount += data.length; + normalizeData(data, opts).then(normalizedData => { + this.#enquedMessages.push([normalizedData, opts?.compress, cb]); + this.#bufferedAmount += normalizedData.length; + }); } } diff --git a/test/js/first_party/ws/ws.test.ts b/test/js/first_party/ws/ws.test.ts index 1325be7ebebe..68621f5ae859 100644 --- a/test/js/first_party/ws/ws.test.ts +++ b/test/js/first_party/ws/ws.test.ts @@ -56,6 +56,10 @@ const binaryTypes = [ label: "arraybuffer", type: ArrayBuffer, }, + { + label: "blob", + type: Blob, + }, ] as const; let servers: Subprocess[] = [];