ws: convert text frames to strings in server socket EventTarget API
Fixes #36060
Repro
import { WebSocketServer } from "ws";
const server = new WebSocketServer({ port: 0 });
server.on("connection", socket => {
socket.on("message", (_data, isBinary) => console.log({ isBinary })); // { isBinary: false }
socket.addEventListener("message", e => console.log(typeof e.data)); // "object" (Buffer), expected "string"
});
A text frame received on a server-side socket is correctly reported as isBinary: false through the EventEmitter API, but addEventListener("message") and the onmessage setter exposed event.data as a Buffer. Node with npm ws@8.x gives a string.
Cause
BunWebSocketMocked in src/js/thirdparty/ws.js converts incoming text to the socket's binaryType representation (a Buffer for the default nodebuffer) before emitting message. The EventTarget-style wrappers (addEventListener and the onmessage setter) then forwarded only the converted data, dropping the isBinary flag that npm ws's event-target shim uses to build the event (data: isBinary ? data : data.toString()).
Fix
The message wrappers now take (data, isBinary) and convert non-binary data back to a string, matching npm ws (verified against ws@8.18.0 on Node 26). The event object also carries type and target like npm ws. While in there:
addEventListenernow honors{ once: true }(previously options were ignored)removeEventListenermatches wrapped listeners the way npmwsdoes, so removing a{ once }listener before it fires works- the
onmessagegetter returns the assigned handler instead of the internal wrapper
Two existing tests asserted the old behavior (e.data equal to Buffer.from("hello") for a text frame via onmessage); they were updated to the npm ws behavior.
Verification
New tests in test/js/first_party/ws/ws.test.ts fail on current bun and pass with this change; all 47 tests in the file pass. The 3 failures in ws-proxy.test.ts pre-exist on main without this diff.
[review] gate passed · iteration 1 · 2 files touched
fails on main (without fix)
ASAN without fix: 9 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/first_party/ws/ws.test.ts
bun test v1.4.0 (47a15b898)
test/js/first_party/ws/ws.test.ts:
Listening: ws://127.0.0.1:34289/
(pass) WebSocket > url [1814.76ms]
Listening: ws://127.0.0.1:39925/
Received upgrade: {
host: "127.0.0.1:39925",
connection: "Upgrade",
upgrade: "websocket",
"sec-websocket-version": "13",
"sec-websocket-extensions": "permessage-deflate; client_max_window_bits",
"sec-websocket-key": "XrrY4vamSw2FQDxR8MWrDw==",
}
Received connection: 127.0.0.1
(pass) WebSocket > readyState [2020.25ms]
Listening: ws://127.0.0.1:39071/
(pass) WebSocket > binaryType > (default) [1768.66ms]
Listening: ws://127.0.0.1:46873/
(pass) WebSocket > binaryType > (invalid) [1772.53ms]
Listening: ws://127.0.0.1:44763/
Received upgrade: {
host: "127.0.0.1:44763",
connection: "Upgrade",
upgrade: "websocket",
"sec-websocket-version": "13",
"sec-websocket-extensions": "permessage-deflate; client_max_window_bits",
"sec-websocket-key": "HMzQbX/GRSKKRbPOWk3yBg==",
}
Received connection: 127.0.0.1
Received message: <Buffer
... (truncated)
release without fix: all passed
bun test v1.4.0-canary.1 (6e98bbffa)
test/js/first_party/ws/ws.test.ts:
Listening: ws://127.0.0.1:34629/
(pass) WebSocket > url [43.12ms]
Listening: ws://127.0.0.1:35259/
Received upgrade: {
host: "127.0.0.1:35259",
connection: "Upgrade",
upgrade: "websocket",
"sec-websocket-version": "13",
"sec-websocket-extensions": "permessage-deflate; client_max_window_bits",
"sec-websocket-key": "K/e8DYEoSrCaZEiy4MKCAg==",
}
Received connection: 127.0.0.1
Received close: 1000
(pass) WebSocket > readyState [47.74ms]
Listening: ws://127.0.0.1:45513/
(pass) WebSocket > binaryType > (default) [31.49ms]
Listening: ws://127.0.0.1:41625/
(pass) WebSocket > binaryType > (invalid) [38.08ms]
Listening: ws://127.0.0.1:35065/
Received upgrade: {
host: "127.0.0.1:35065",
connection: "Upgrade",
upgrade: "websocket",
"sec-websocket-version": "13",
"sec-websocket-extensions": "permessage-deflate; client_max_window_bits",
"sec-websocket-key": "+UYmZi75SlObjh5KL0kQNw==",
}
Received connection: 127.0.0.1
Received message: <Buffer 00>
Received ping: <Buffer >
Received pong: <Buffer >
(pass) WebSocket > binaryType > nodebuffer [43.11ms]
Listening: ws://127.0.0.1:38181/
Rec
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/first_party/ws/ws.test.ts
bun test v1.4.0 (47a15b898)
test/js/first_party/ws/ws.test.ts:
Listening: ws://127.0.0.1:36973/
(pass) WebSocket > url [1804.80ms]
Listening: ws://127.0.0.1:34165/
Received upgrade: {
host: "127.0.0.1:34165",
connection: "Upgrade",
upgrade: "websocket",
"sec-websocket-version": "13",
"sec-websocket-extensions": "permessage-deflate; client_max_window_bits",
"sec-websocket-key": "YHgorM9CQoqtB5HW1K06Sw==",
}
Received connection: 127.0.0.1
(pass) WebSocket > readyState [2030.89ms]
Listening: ws://127.0.0.1:34505/
(pass) WebSocket > binaryType > (default) [1737.75ms]
Listening: ws://127.0.0.1:41013/
(pass) WebSocket > binaryType > (invalid) [1756.17ms]
Listening: ws://127.0.0.1:33729/
Received upgrade: {
host: "127.0.0.1:33729",
connection: "Upgrade",
upgrade: "websocket",
"sec-websocket-version": "13",
"sec-websocket-extensions": "permessage-deflate; client_max_window_bits",
"sec-websocket-key": "zNhwX1f9Qvi6nepKPQgmyA==",
}
Received connection: 127.0.0.1
Received message: <Buffer
... (truncated)
release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 759ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/22] gen generated_host_exports.rs
generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 240 extern-C blocks audited
[2/22] gen JS modules (bundle-modules)
Preprocess modules (8916ms)
Bundle modules (91ms)
Postprocesss modules (216ms)
Bundle Functions (744ms)
Generate Code (30ms)
[10.02s] Bundled "src/js" for production
2571 kb
193 internal modules
13 native modules
90 internal functions across 19 files
[2/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)
[1m[92m Compiling[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
[1m[92m Compiling[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
[1m[92m Compiling[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
[1m[92m Compiling[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
[1m[92m Compiling[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
[1m[92m Compiling[0m bun_zlib_s
... (truncated)
diff hotspot
src/js/thirdparty/ws.js | 56 +++++++++----
test/js/first_party/ws/ws.test.ts | 170 +++++++++++++++++++++++++++++++++++++-
2 files changed, 205 insertions(+), 21 deletions(-)
gate history · 3 passed · 0 rejected · iteration 1
evidence per changed file
file reads edits tests
src/js/thirdparty/ws.js 7 10 0
test/js/first_party/ws/ws.test.ts 4 9 0