From b8b8fec45f352a4972652f947efd8b7451f0b651 Mon Sep 17 00:00:00 2001 From: ryuhei shima Date: Fri, 13 Mar 2026 16:58:41 +0900 Subject: [PATCH 01/14] feat(websocket): expand diagnostics channel payloads --- docs/docs/api/DiagnosticsChannel.md | 72 ++++++++++++++++++- lib/core/diagnostics.js | 42 +++++++++-- lib/web/websocket/connection.js | 10 +++ lib/web/websocket/receiver.js | 35 ++++++++- lib/web/websocket/sender.js | 68 +++++++++++++++++- lib/web/websocket/stream/websocketstream.js | 5 +- lib/web/websocket/websocket.js | 16 ++++- test/types/diagnostics-channel.test-d.ts | 48 ++++++++++++- ...stics-channel-created-handshake-request.js | 64 +++++++++++++++++ .../diagnostics-channel-frame-error.js | 42 +++++++++++ test/websocket/diagnostics-channel-frames.js | 57 +++++++++++++++ types/diagnostics-channel.d.ts | 45 ++++++++++++ 12 files changed, 490 insertions(+), 14 deletions(-) create mode 100644 test/websocket/diagnostics-channel-created-handshake-request.js create mode 100644 test/websocket/diagnostics-channel-frame-error.js create mode 100644 test/websocket/diagnostics-channel-frames.js diff --git a/docs/docs/api/DiagnosticsChannel.md b/docs/docs/api/DiagnosticsChannel.md index acf25e08218..30ede81a127 100644 --- a/docs/docs/api/DiagnosticsChannel.md +++ b/docs/docs/api/DiagnosticsChannel.md @@ -201,6 +201,32 @@ The `handshakeResponse` object contains the HTTP response that upgraded the conn This information is particularly useful for debugging and monitoring WebSocket connections, as it provides access to the initial HTTP handshake response that established the WebSocket connection. +## `undici:websocket:created` + +This message is published when a `WebSocket` instance is created, before the opening handshake is sent. + +```js +import diagnosticsChannel from 'diagnostics_channel' + +diagnosticsChannel.channel('undici:websocket:created').subscribe(({ websocket, url }) => { + console.log(websocket) // the WebSocket instance + console.log(url) // serialized websocket URL +}) +``` + +## `undici:websocket:handshakeRequest` + +This message is published when the HTTP upgrade request is about to be sent. + +```js +import diagnosticsChannel from 'diagnostics_channel' + +diagnosticsChannel.channel('undici:websocket:handshakeRequest').subscribe(({ websocket, request }) => { + console.log(websocket) // the WebSocket instance + console.log(request.headers) // handshake request headers assembled so far +}) +``` + ## `undici:websocket:close` This message is published after the connection has closed. @@ -222,8 +248,52 @@ This message is published if the socket experiences an error. ```js import diagnosticsChannel from 'diagnostics_channel' -diagnosticsChannel.channel('undici:websocket:socket_error').subscribe((error) => { +diagnosticsChannel.channel('undici:websocket:socket_error').subscribe(({ error, websocket }) => { console.log(error) + console.log(websocket) // the WebSocket instance, if available +}) +``` + +## `undici:websocket:frameSent` + +This message is published after a WebSocket frame is written to the socket. + +```js +import diagnosticsChannel from 'diagnostics_channel' + +diagnosticsChannel.channel('undici:websocket:frameSent').subscribe(({ websocket, opcode, mask, payloadData }) => { + console.log(websocket) // the WebSocket instance + console.log(opcode) // RFC 6455 opcode + console.log(mask) // true for client-sent frames + console.log(payloadData) // unmasked payload bytes +}) +``` + +## `undici:websocket:frameReceived` + +This message is published after a WebSocket frame is parsed from the socket. + +```js +import diagnosticsChannel from 'diagnostics_channel' + +diagnosticsChannel.channel('undici:websocket:frameReceived').subscribe(({ websocket, opcode, mask, payloadData }) => { + console.log(websocket) // the WebSocket instance + console.log(opcode) // RFC 6455 opcode + console.log(mask) // false for server-sent frames + console.log(payloadData) // payload bytes as received +}) +``` + +## `undici:websocket:frameError` + +This message is published when Undici rejects an invalid incoming WebSocket frame. + +```js +import diagnosticsChannel from 'diagnostics_channel' + +diagnosticsChannel.channel('undici:websocket:frameError').subscribe(({ websocket, error }) => { + console.log(websocket) // the WebSocket instance + console.log(error.message) }) ``` diff --git a/lib/core/diagnostics.js b/lib/core/diagnostics.js index ccd6870ca6d..c4697625d50 100644 --- a/lib/core/diagnostics.js +++ b/lib/core/diagnostics.js @@ -22,8 +22,13 @@ const channels = { trailers: diagnosticsChannel.channel('undici:request:trailers'), error: diagnosticsChannel.channel('undici:request:error'), // WebSocket + created: diagnosticsChannel.channel('undici:websocket:created'), + handshakeRequest: diagnosticsChannel.channel('undici:websocket:handshakeRequest'), open: diagnosticsChannel.channel('undici:websocket:open'), close: diagnosticsChannel.channel('undici:websocket:close'), + frameSent: diagnosticsChannel.channel('undici:websocket:frameSent'), + frameReceived: diagnosticsChannel.channel('undici:websocket:frameReceived'), + frameError: diagnosticsChannel.channel('undici:websocket:frameError'), socketError: diagnosticsChannel.channel('undici:websocket:socket_error'), ping: diagnosticsChannel.channel('undici:websocket:ping'), pong: diagnosticsChannel.channel('undici:websocket:pong'), @@ -166,15 +171,27 @@ function trackWebSocketEvents (debugLog = websocketDebuglog) { // Check if any of the channels already have subscribers to prevent duplicate subscriptions // This can happen when both Node.js built-in undici and undici as a dependency are present - if (channels.open.hasSubscribers || channels.close.hasSubscribers || - channels.socketError.hasSubscribers || channels.ping.hasSubscribers || - channels.pong.hasSubscribers) { + if (channels.created.hasSubscribers || channels.handshakeRequest.hasSubscribers || + channels.open.hasSubscribers || channels.close.hasSubscribers || + channels.frameSent.hasSubscribers || channels.frameReceived.hasSubscribers || + channels.frameError.hasSubscribers || channels.socketError.hasSubscribers || + channels.ping.hasSubscribers || channels.pong.hasSubscribers) { isTrackingWebSocketEvents = true return } isTrackingWebSocketEvents = true + diagnosticsChannel.subscribe('undici:websocket:created', + evt => { + debugLog('created websocket for %s', evt.url) + }) + + diagnosticsChannel.subscribe('undici:websocket:handshakeRequest', + evt => { + debugLog('sending opening handshake for %s', evt.websocket?.url ?? '') + }) + diagnosticsChannel.subscribe('undici:websocket:open', evt => { const { @@ -194,9 +211,24 @@ function trackWebSocketEvents (debugLog = websocketDebuglog) { ) }) + diagnosticsChannel.subscribe('undici:websocket:frameSent', + evt => { + debugLog('frame sent opcode=%d bytes=%d', evt.opcode, evt.payloadData.length) + }) + + diagnosticsChannel.subscribe('undici:websocket:frameReceived', + evt => { + debugLog('frame received opcode=%d bytes=%d', evt.opcode, evt.payloadData.length) + }) + + diagnosticsChannel.subscribe('undici:websocket:frameError', + evt => { + debugLog('frame errored for %s - %s', evt.websocket?.url ?? '', evt.error.message) + }) + diagnosticsChannel.subscribe('undici:websocket:socket_error', - err => { - debugLog('connection errored - %s', err.message) + evt => { + debugLog('connection errored for %s - %s', evt.websocket?.url ?? '', evt.error.message) }) diagnosticsChannel.subscribe('undici:websocket:ping', diff --git a/lib/web/websocket/connection.js b/lib/web/websocket/connection.js index 4ecc8a195fc..107b5caffe2 100644 --- a/lib/web/websocket/connection.js +++ b/lib/web/websocket/connection.js @@ -9,6 +9,7 @@ const { getDecodeSplit } = require('../fetch/util') const { WebsocketFrameSend } = require('./frame') const assert = require('node:assert') const { runtimeFeatures } = require('../../util/runtime-features') +const { channels } = require('../../core/diagnostics') const crypto = runtimeFeatures.has('crypto') ? require('node:crypto') @@ -89,6 +90,15 @@ function establishWebSocketConnection (url, protocols, client, handler, options) // 11. Fetch request with useParallelQueue set to true, and // processResponse given response being these steps: + if (channels.handshakeRequest.hasSubscribers) { + channels.handshakeRequest.publish({ + websocket: handler.websocket, + request: { + headers: request.headersList.entries + } + }) + } + const controller = fetching({ request, useParallelQueue: true, diff --git a/lib/web/websocket/receiver.js b/lib/web/websocket/receiver.js index 13ad8b48201..4990b63d8cd 100644 --- a/lib/web/websocket/receiver.js +++ b/lib/web/websocket/receiver.js @@ -16,6 +16,7 @@ const { failWebsocketConnection } = require('./connection') const { WebsocketFrameSend } = require('./frame') const { PerMessageDeflate } = require('./permessage-deflate') const { MessageSizeExceededError } = require('../../core/errors') +const { channels } = require('../../core/diagnostics') // This code was influenced by ws released under the MIT license. // Copyright (c) 2011 Einar Otto Stangvik @@ -97,11 +98,13 @@ class ByteParser extends Writable { const rsv3 = buffer[0] & 0x10 if (!isValidOpcode(opcode)) { + this.publishFrameError(new Error('Invalid opcode received')) failWebsocketConnection(this.#handler, 1002, 'Invalid opcode received') return callback() } if (masked) { + this.publishFrameError(new Error('Frame cannot be masked')) failWebsocketConnection(this.#handler, 1002, 'Frame cannot be masked') return callback() } @@ -116,17 +119,20 @@ class ByteParser extends Writable { // WebSocket connection where a PMCE is in use, this bit indicates // whether a message is compressed or not. if (rsv1 !== 0 && !this.#extensions.has('permessage-deflate')) { + this.publishFrameError(new Error('Expected RSV1 to be clear.')) failWebsocketConnection(this.#handler, 1002, 'Expected RSV1 to be clear.') return } if (rsv2 !== 0 || rsv3 !== 0) { + this.publishFrameError(new Error('RSV1, RSV2, RSV3 must be clear')) failWebsocketConnection(this.#handler, 1002, 'RSV1, RSV2, RSV3 must be clear') return } if (fragmented && !isTextBinaryFrame(opcode)) { // Only text and binary frames can be fragmented + this.publishFrameError(new Error('Invalid frame type was fragmented.')) failWebsocketConnection(this.#handler, 1002, 'Invalid frame type was fragmented.') return } @@ -134,12 +140,14 @@ class ByteParser extends Writable { // If we are already parsing a text/binary frame and do not receive either // a continuation frame or close frame, fail the connection. if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) { + this.publishFrameError(new Error('Expected continuation frame')) failWebsocketConnection(this.#handler, 1002, 'Expected continuation frame') return } if (this.#info.fragmented && fragmented) { // A fragmented frame can't be fragmented itself + this.publishFrameError(new Error('Fragmented frame exceeded 125 bytes.')) failWebsocketConnection(this.#handler, 1002, 'Fragmented frame exceeded 125 bytes.') return } @@ -147,11 +155,13 @@ class ByteParser extends Writable { // "All control frames MUST have a payload length of 125 bytes or less // and MUST NOT be fragmented." if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) { + this.publishFrameError(new Error('Control frame either too large or fragmented')) failWebsocketConnection(this.#handler, 1002, 'Control frame either too large or fragmented') return } if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) { + this.publishFrameError(new Error('Unexpected continuation frame')) failWebsocketConnection(this.#handler, 1002, 'Unexpected continuation frame') return } @@ -199,6 +209,7 @@ class ByteParser extends Writable { // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275 // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e if (upper !== 0 || lower > 2 ** 31 - 1) { + this.publishFrameError(new Error('Received payload length > 2^31 bytes.')) failWebsocketConnection(this.#handler, 1009, 'Received payload length > 2^31 bytes.') return } @@ -212,6 +223,15 @@ class ByteParser extends Writable { const body = this.consume(this.#info.payloadLength) + if (channels.frameReceived.hasSubscribers) { + channels.frameReceived.publish({ + websocket: this.#handler.websocket, + opcode: this.#info.opcode, + mask: this.#info.masked, + payloadData: Buffer.from(body) + }) + } + if (isControlFrame(this.#info.opcode)) { this.#loop = this.parseControlFrame(body) this.#state = parserStates.INFO @@ -231,7 +251,7 @@ class ByteParser extends Writable { } else { this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => { if (error) { - // Use 1009 (Message Too Big) for decompression size limit errors + this.publishFrameError(error) const code = error instanceof MessageSizeExceededError ? 1009 : 1007 failWebsocketConnection(this.#handler, code, error.message) return @@ -384,6 +404,7 @@ class ByteParser extends Writable { if (opcode === opcodes.CLOSE) { if (payloadLength === 1) { + this.publishFrameError(new Error('Received close frame with a 1-byte body.')) failWebsocketConnection(this.#handler, 1002, 'Received close frame with a 1-byte body.') return false } @@ -393,6 +414,7 @@ class ByteParser extends Writable { if (this.#info.closeInfo.error) { const { code, reason } = this.#info.closeInfo + this.publishFrameError(new Error(reason)) failWebsocketConnection(this.#handler, code, reason) return false } @@ -448,6 +470,17 @@ class ByteParser extends Writable { get closingInfo () { return this.#info.closeInfo } + + publishFrameError (error) { + if (!channels.frameError.hasSubscribers) { + return + } + + channels.frameError.publish({ + websocket: this.#handler.websocket, + error + }) + } } module.exports = { diff --git a/lib/web/websocket/sender.js b/lib/web/websocket/sender.js index c647bf629d7..a62f7625af5 100644 --- a/lib/web/websocket/sender.js +++ b/lib/web/websocket/sender.js @@ -3,12 +3,14 @@ const { WebsocketFrameSend } = require('./frame') const { opcodes, sendHints } = require('./constants') const FixedQueue = require('../../dispatcher/fixed-queue') +const { channels } = require('../../core/diagnostics') /** * @typedef {object} SendQueueNode * @property {Promise | null} promise * @property {((...args: any[]) => any)} callback * @property {Buffer | null} frame + * @property {boolean} published */ class SendQueue { @@ -25,8 +27,11 @@ class SendQueue { /** @type {import('node:net').Socket} */ #socket - constructor (socket) { + #websocket + + constructor (socket, websocket) { this.#socket = socket + this.#websocket = websocket } add (item, cb, hint) { @@ -35,6 +40,7 @@ class SendQueue { // TODO(@tsctx): support fast-path for string on running if (hint === sendHints.text) { // special fast-path for string + publishFrame(this.#websocket, opcodes.TEXT, true, item) const { 0: head, 1: body } = WebsocketFrameSend.createFastTextFrame(item) this.#socket.cork() this.#socket.write(head) @@ -42,6 +48,7 @@ class SendQueue { this.#socket.uncork() } else { // direct writing + publishFrame(this.#websocket, hint === sendHints.text ? opcodes.TEXT : opcodes.BINARY, true, toBuffer(item, hint)) this.#socket.write(createFrame(item, hint), cb) } } else { @@ -49,7 +56,8 @@ class SendQueue { const node = { promise: null, callback: cb, - frame: createFrame(item, hint) + frame: createFrame(item, hint), + published: false } this.#queue.push(node) } @@ -60,10 +68,13 @@ class SendQueue { const node = { promise: item.arrayBuffer().then((ab) => { node.promise = null + publishFrame(this.#websocket, opcodes.BINARY, true, new Uint8Array(ab)) + node.published = true node.frame = createFrame(ab, hint) }), callback: cb, - frame: null + frame: null, + published: false } this.#queue.push(node) @@ -83,6 +94,9 @@ class SendQueue { await node.promise } // write + if (node.frame !== null && !node.published) { + publishQueuedFrame(this.#websocket, node.frame) + } this.#socket.write(node.frame, node.callback) // cleanup node.callback = node.frame = null @@ -106,4 +120,52 @@ function toBuffer (data, hint) { } } +function publishFrame (websocket, opcode, mask, payloadData) { + if (!channels.frameSent.hasSubscribers) { + return + } + + channels.frameSent.publish({ + websocket, + opcode, + mask, + payloadData: Buffer.from(payloadData) + }) +} + +function publishQueuedFrame (websocket, frame) { + if (!channels.frameSent.hasSubscribers) { + return + } + + const payloadOffset = getPayloadOffset(frame) + const payloadData = Buffer.allocUnsafe(frame.length - payloadOffset) + const maskKey = frame.subarray(payloadOffset - 4, payloadOffset) + + for (let i = 0; i < payloadData.length; ++i) { + payloadData[i] = frame[payloadOffset + i] ^ maskKey[i & 3] + } + + channels.frameSent.publish({ + websocket, + opcode: frame[0] & 0x0F, + mask: (frame[1] & 0x80) === 0x80, + payloadData + }) +} + +function getPayloadOffset (frame) { + const payloadLength = frame[1] & 0x7F + + if (payloadLength === 126) { + return 8 + } + + if (payloadLength === 127) { + return 14 + } + + return 6 +} + module.exports = { SendQueue } diff --git a/lib/web/websocket/stream/websocketstream.js b/lib/web/websocket/stream/websocketstream.js index ce3be84fc3d..dc0b053edee 100644 --- a/lib/web/websocket/stream/websocketstream.js +++ b/lib/web/websocket/stream/websocketstream.js @@ -57,7 +57,10 @@ class WebSocketStream { this.#handler.readyState = states.CLOSING if (channels.socketError.hasSubscribers) { - channels.socketError.publish(err) + channels.socketError.publish({ + error: err, + websocket: undefined + }) } this.#handler.socket.destroy() diff --git a/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index 676b20164df..7fb3d3e4408 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -36,6 +36,7 @@ const { channels } = require('../../core/diagnostics') * @property {() => void} onSocketClose * @property {(body: Buffer) => void} onPing * @property {(body: Buffer) => void} onPong + * @property {WebSocket} [websocket] * * @property {number} readyState * @property {import('stream').Duplex} socket @@ -75,7 +76,10 @@ class WebSocket extends EventTarget { this.#handler.readyState = states.CLOSING if (channels.socketError.hasSubscribers) { - channels.socketError.publish(err) + channels.socketError.publish({ + error: err, + websocket: this + }) } this.#handler.socket.destroy() @@ -155,6 +159,14 @@ class WebSocket extends EventTarget { // 5. Set this's url to urlRecord. this.#url = new URL(urlRecord.href) + this.#handler.websocket = this + + if (channels.created.hasSubscribers) { + channels.created.publish({ + websocket: this, + url: URLSerializer(this.#url) + }) + } // Store options for later use (e.g., maxDecompressedMessageSize) this.#options = { @@ -468,7 +480,7 @@ class WebSocket extends EventTarget { parser.on('error', (err) => this.#handler.onParserError(err)) this.#parser = parser - this.#sendQueue = new SendQueue(response.socket) + this.#sendQueue = new SendQueue(response.socket, this) // 1. Change the ready state to OPEN (1). this.#handler.readyState = states.OPEN diff --git a/test/types/diagnostics-channel.test-d.ts b/test/types/diagnostics-channel.test-d.ts index 842788c6b93..f35411139f1 100644 --- a/test/types/diagnostics-channel.test-d.ts +++ b/test/types/diagnostics-channel.test-d.ts @@ -1,6 +1,6 @@ import { Socket } from 'node:net' import { expectAssignable } from 'tsd' -import { DiagnosticsChannel, buildConnector } from '../..' +import { DiagnosticsChannel, WebSocket, buildConnector } from '../..' const request = { origin: '', @@ -27,6 +27,8 @@ const connectParams = { servername: '' } +const websocket = {} as InstanceType + expectAssignable({ request }) expectAssignable({ request, chunk: Buffer.from('') }) expectAssignable({ request, chunk: '' }) @@ -73,3 +75,47 @@ expectAssignable({ callback: buildConnector.Callback ) => new Socket() }) +expectAssignable({ + websocket, + url: 'ws://localhost:3000' +}) +expectAssignable({ + websocket, + request: { + headers: {} + } +}) +expectAssignable({ + address: { + address: '127.0.0.1', + family: 'IPv4', + port: 3000 + }, + protocol: '', + extensions: '', + websocket, + handshakeResponse: { + status: 101, + statusText: 'Switching Protocols', + headers: {} + } +}) +expectAssignable({ + websocket, + code: 1000, + reason: '' +}) +expectAssignable({ + websocket, + opcode: 1, + mask: true, + payloadData: Buffer.from('') +}) +expectAssignable({ + websocket, + error: new Error('Error') +}) +expectAssignable({ + websocket, + error: new Error('Error') +}) diff --git a/test/websocket/diagnostics-channel-created-handshake-request.js b/test/websocket/diagnostics-channel-created-handshake-request.js new file mode 100644 index 00000000000..fa9e2c415ac --- /dev/null +++ b/test/websocket/diagnostics-channel-created-handshake-request.js @@ -0,0 +1,64 @@ +'use strict' + +const { test } = require('node:test') +const { once } = require('node:events') +const { createServer } = require('node:http') +const dc = require('node:diagnostics_channel') +const { WebSocketServer } = require('ws') +const { WebSocket } = require('../..') + +test('diagnostics channel - undici:websocket:[created/handshakeRequest]', async (t) => { + t.plan(10) + + const server = createServer() + const wss = new WebSocketServer({ noServer: true }) + + server.on('upgrade', (request, socket, head) => { + wss.handleUpgrade(request, socket, head, (ws) => { + ws.close(1000, 'done') + }) + }) + + server.listen(0) + await once(server, 'listening') + + const url = `ws://localhost:${server.address().port}` + const events = [] + let createdWebSocket + let handshakeWebSocket + + const createdListener = ({ websocket, url: createdUrl }) => { + events.push('created') + createdWebSocket = websocket + t.assert.strictEqual(createdUrl, `${url}/`) + } + + const handshakeRequestListener = ({ websocket, request }) => { + events.push('handshakeRequest') + handshakeWebSocket = websocket + t.assert.strictEqual(typeof request, 'object') + t.assert.strictEqual(typeof request.headers, 'object') + t.assert.strictEqual(request.headers['sec-websocket-version'], '13') + t.assert.strictEqual(request.headers['sec-websocket-extensions'], 'permessage-deflate; client_max_window_bits') + t.assert.strictEqual(typeof request.headers['sec-websocket-key'], 'string') + } + + dc.channel('undici:websocket:created').subscribe(createdListener) + dc.channel('undici:websocket:handshakeRequest').subscribe(handshakeRequestListener) + + const ws = new WebSocket(url) + + t.after(() => { + dc.channel('undici:websocket:created').unsubscribe(createdListener) + dc.channel('undici:websocket:handshakeRequest').unsubscribe(handshakeRequestListener) + wss.close() + server.close() + }) + + await once(ws, 'close') + + t.assert.deepStrictEqual(events, ['created', 'handshakeRequest']) + t.assert.strictEqual(createdWebSocket, ws) + t.assert.strictEqual(handshakeWebSocket, ws) + t.assert.strictEqual(ws.url, `${url}/`) +}) diff --git a/test/websocket/diagnostics-channel-frame-error.js b/test/websocket/diagnostics-channel-frame-error.js new file mode 100644 index 00000000000..4305c43550d --- /dev/null +++ b/test/websocket/diagnostics-channel-frame-error.js @@ -0,0 +1,42 @@ +'use strict' + +const { test } = require('node:test') +const { once } = require('node:events') +const dc = require('node:diagnostics_channel') +const { WebSocketServer } = require('ws') +const { WebSocket } = require('../..') +const { WebsocketFrameSend } = require('../../lib/web/websocket/frame') + +test('diagnostics channel - undici:websocket:frameError', async (t) => { + t.plan(3) + + const body = Buffer.allocUnsafe(2) + body.writeUInt16BE(1006, 0) + + const frame = new WebsocketFrameSend(body) + const buffer = frame.createFrame(0x8) + + const server = new WebSocketServer({ port: 0 }) + + server.on('connection', (socket) => { + socket._socket.write(buffer, () => socket.close()) + }) + + const ws = new WebSocket(`ws://localhost:${server.address().port}`) + + const frameErrorListener = ({ websocket, error }) => { + t.assert.strictEqual(websocket, ws) + t.assert.strictEqual(error.message, 'Frame cannot be masked') + } + + dc.channel('undici:websocket:frameError').subscribe(frameErrorListener) + + t.after(() => { + server.close() + ws.close() + dc.channel('undici:websocket:frameError').unsubscribe(frameErrorListener) + }) + + await once(ws, 'close') + t.assert.strictEqual(ws.readyState, WebSocket.CLOSED) +}) diff --git a/test/websocket/diagnostics-channel-frames.js b/test/websocket/diagnostics-channel-frames.js new file mode 100644 index 00000000000..4563e8e6e19 --- /dev/null +++ b/test/websocket/diagnostics-channel-frames.js @@ -0,0 +1,57 @@ +'use strict' + +const { test } = require('node:test') +const { once } = require('node:events') +const dc = require('node:diagnostics_channel') +const { WebSocketServer } = require('ws') +const { WebSocket } = require('../..') +const { opcodes } = require('../../lib/web/websocket/constants') + +test('diagnostics channel - undici:websocket:[frameSent/frameReceived]', async (t) => { + t.plan(8) + + const server = new WebSocketServer({ port: 0 }) + const ws = new WebSocket(`ws://localhost:${server.address().port}`) + + server.on('connection', (socket) => { + socket.on('message', (payload) => { + socket.send(payload.toString()) + }) + }) + + const frameSentListener = ({ websocket, opcode, mask, payloadData }) => { + if (opcode !== opcodes.TEXT || payloadData.toString() !== 'hello') { + return + } + + t.assert.strictEqual(websocket, ws) + t.assert.strictEqual(opcode, opcodes.TEXT) + t.assert.strictEqual(mask, true) + t.assert.strictEqual(payloadData.toString(), 'hello') + } + + const frameReceivedListener = ({ websocket, opcode, mask, payloadData }) => { + if (opcode !== opcodes.TEXT || payloadData.toString() !== 'hello') { + return + } + + t.assert.strictEqual(websocket, ws) + t.assert.strictEqual(opcode, opcodes.TEXT) + t.assert.strictEqual(mask, false) + t.assert.strictEqual(payloadData.toString(), 'hello') + } + + dc.channel('undici:websocket:frameSent').subscribe(frameSentListener) + dc.channel('undici:websocket:frameReceived').subscribe(frameReceivedListener) + + t.after(() => { + server.close() + ws.close() + dc.channel('undici:websocket:frameSent').unsubscribe(frameSentListener) + dc.channel('undici:websocket:frameReceived').unsubscribe(frameReceivedListener) + }) + + await once(ws, 'open') + ws.send('hello') + await once(ws, 'message') +}) diff --git a/types/diagnostics-channel.d.ts b/types/diagnostics-channel.d.ts index 3c6a5299d38..e149e76054b 100644 --- a/types/diagnostics-channel.d.ts +++ b/types/diagnostics-channel.d.ts @@ -4,6 +4,7 @@ import buildConnector from './connector' import Dispatcher from './dispatcher' declare namespace DiagnosticsChannel { + type WebSocket = InstanceType interface Request { origin?: string | URL; completed: boolean; @@ -71,4 +72,48 @@ declare namespace DiagnosticsChannel { connectParams: ConnectParams; connector: Connector; } + export interface WebsocketCreatedMessage { + websocket: WebSocket; + url: string; + } + export interface WebsocketHandshakeRequestMessage { + websocket: WebSocket; + request: { + headers: Record; + }; + } + export interface WebsocketOpenMessage { + address: { + address: string; + family: string; + port: number; + }; + protocol: string; + extensions: string; + websocket: WebSocket; + handshakeResponse: { + status: number; + statusText: string; + headers: Record; + }; + } + export interface WebsocketCloseMessage { + websocket: WebSocket; + code: number; + reason: string; + } + export interface WebsocketFrameMessage { + websocket: WebSocket; + opcode: number; + mask: boolean; + payloadData: Buffer; + } + export interface WebsocketFrameErrorMessage { + websocket: WebSocket; + error: Error; + } + export interface WebsocketSocketErrorMessage { + websocket?: WebSocket; + error: Error; + } } From 639d09ffcf583ee8ca8855fac6f0c1039aab1bbf Mon Sep 17 00:00:00 2001 From: ryuhei shima Date: Fri, 13 Mar 2026 20:03:56 +0900 Subject: [PATCH 02/14] update websocket debug expectations --- test/node-test/debug.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/node-test/debug.js b/test/node-test/debug.js index 2e047175307..38ec31936ec 100644 --- a/test/node-test/debug.js +++ b/test/node-test/debug.js @@ -12,7 +12,7 @@ const isNode23Plus = process.versions.node.split('.')[0] >= 23 const isCITGM = !!process.env.CITGM test('debug#websocket', { skip: !process.versions.icu || isCITGM || isNode23Plus }, async t => { - const assert = tspl(t, { plan: 6 }) + const assert = tspl(t, { plan: 10 }) const child = spawn( process.execPath, [ @@ -26,10 +26,13 @@ test('debug#websocket', { skip: !process.versions.icu || isCITGM || isNode23Plus ) const chunks = [] const assertions = [ + /(WEBSOCKET [0-9]+:) (created websocket for)/, + /(WEBSOCKET [0-9]+:) (sending opening handshake for)/, /(WEBSOCKET [0-9]+:) (connecting to)/, /(WEBSOCKET [0-9]+:) (connected to)/, /(WEBSOCKET [0-9]+:) (sending request)/, /(WEBSOCKET [0-9]+:) (connection opened)/, + /(WEBSOCKET [0-9]+:) (frame received opcode=8 bytes=9)/, /(WEBSOCKET [0-9]+:) (closed connection to)/, /^$/ ] @@ -41,7 +44,7 @@ test('debug#websocket', { skip: !process.versions.icu || isCITGM || isNode23Plus child.stderr.on('end', () => { const lines = extractLines(chunks) assert.strictEqual(lines.length, assertions.length) - for (let i = 1; i < lines.length; i++) { + for (let i = 0; i < lines.length; i++) { assert.match(lines[i], assertions[i]) } }) From aa50095f19f8c61b8206c376725f62365f7425c4 Mon Sep 17 00:00:00 2001 From: ryuhei shima Date: Sat, 14 Mar 2026 20:48:52 +0900 Subject: [PATCH 03/14] reduce frameSent diagnostics overhead --- lib/web/websocket/sender.js | 57 ++++++++++++++----------------------- 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/lib/web/websocket/sender.js b/lib/web/websocket/sender.js index a62f7625af5..1c9c9e420a9 100644 --- a/lib/web/websocket/sender.js +++ b/lib/web/websocket/sender.js @@ -10,7 +10,7 @@ const { channels } = require('../../core/diagnostics') * @property {Promise | null} promise * @property {((...args: any[]) => any)} callback * @property {Buffer | null} frame - * @property {boolean} published + * @property {{ published: boolean, data: Buffer | ArrayBuffer | ArrayBufferView | null, hint: number }} diagnosticInfo */ class SendQueue { @@ -40,7 +40,7 @@ class SendQueue { // TODO(@tsctx): support fast-path for string on running if (hint === sendHints.text) { // special fast-path for string - publishFrame(this.#websocket, opcodes.TEXT, true, item) + publishFrame(this.#websocket, opcodes.TEXT, true, item, hint) const { 0: head, 1: body } = WebsocketFrameSend.createFastTextFrame(item) this.#socket.cork() this.#socket.write(head) @@ -48,7 +48,7 @@ class SendQueue { this.#socket.uncork() } else { // direct writing - publishFrame(this.#websocket, hint === sendHints.text ? opcodes.TEXT : opcodes.BINARY, true, toBuffer(item, hint)) + publishFrame(this.#websocket, hint === sendHints.text ? opcodes.TEXT : opcodes.BINARY, true, item, hint) this.#socket.write(createFrame(item, hint), cb) } } else { @@ -57,7 +57,11 @@ class SendQueue { promise: null, callback: cb, frame: createFrame(item, hint), - published: false + diagnosticInfo: { + published: false, + data: item, + hint + } } this.#queue.push(node) } @@ -68,13 +72,17 @@ class SendQueue { const node = { promise: item.arrayBuffer().then((ab) => { node.promise = null - publishFrame(this.#websocket, opcodes.BINARY, true, new Uint8Array(ab)) - node.published = true + publishFrame(this.#websocket, opcodes.BINARY, true, ab, hint) + node.diagnosticInfo.published = true node.frame = createFrame(ab, hint) }), callback: cb, frame: null, - published: false + diagnosticInfo: { + published: false, + data: item, + hint + } } this.#queue.push(node) @@ -94,8 +102,9 @@ class SendQueue { await node.promise } // write - if (node.frame !== null && !node.published) { - publishQueuedFrame(this.#websocket, node.frame) + if (node.frame !== null && !node.diagnosticInfo.published) { + publishQueuedFrame(this.#websocket, node.frame, node.diagnosticInfo) + node.diagnosticInfo.published = true } this.#socket.write(node.frame, node.callback) // cleanup @@ -120,7 +129,7 @@ function toBuffer (data, hint) { } } -function publishFrame (websocket, opcode, mask, payloadData) { +function publishFrame (websocket, opcode, mask, data, hint) { if (!channels.frameSent.hasSubscribers) { return } @@ -129,43 +138,21 @@ function publishFrame (websocket, opcode, mask, payloadData) { websocket, opcode, mask, - payloadData: Buffer.from(payloadData) + payloadData: Buffer.from(toBuffer(data, hint)) }) } -function publishQueuedFrame (websocket, frame) { +function publishQueuedFrame (websocket, frame, diagnosticInfo) { if (!channels.frameSent.hasSubscribers) { return } - const payloadOffset = getPayloadOffset(frame) - const payloadData = Buffer.allocUnsafe(frame.length - payloadOffset) - const maskKey = frame.subarray(payloadOffset - 4, payloadOffset) - - for (let i = 0; i < payloadData.length; ++i) { - payloadData[i] = frame[payloadOffset + i] ^ maskKey[i & 3] - } - channels.frameSent.publish({ websocket, opcode: frame[0] & 0x0F, mask: (frame[1] & 0x80) === 0x80, - payloadData + payloadData: Buffer.from(toBuffer(diagnosticInfo.data, diagnosticInfo.hint)) }) } -function getPayloadOffset (frame) { - const payloadLength = frame[1] & 0x7F - - if (payloadLength === 126) { - return 8 - } - - if (payloadLength === 127) { - return 14 - } - - return 6 -} - module.exports = { SendQueue } From b8d7ae5afa3f1e37674b049376724ada8564eea5 Mon Sep 17 00:00:00 2001 From: Ryuhei Shima <65934663+islandryu@users.noreply.github.com> Date: Sat, 21 Mar 2026 21:37:36 +0900 Subject: [PATCH 04/14] Update lib/web/websocket/sender.js Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> --- lib/web/websocket/sender.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web/websocket/sender.js b/lib/web/websocket/sender.js index 1c9c9e420a9..ba2670744f9 100644 --- a/lib/web/websocket/sender.js +++ b/lib/web/websocket/sender.js @@ -48,7 +48,7 @@ class SendQueue { this.#socket.uncork() } else { // direct writing - publishFrame(this.#websocket, hint === sendHints.text ? opcodes.TEXT : opcodes.BINARY, true, item, hint) + publishFrame(this.#websocket, opcodes.BINARY, true, item, hint) this.#socket.write(createFrame(item, hint), cb) } } else { From 0f5ae89ce2b9c52fd82018c7a89a9f83dd0a96ce Mon Sep 17 00:00:00 2001 From: ryuhei shima Date: Sat, 21 Mar 2026 21:42:57 +0900 Subject: [PATCH 05/14] remove websocket from socket_error --- docs/docs/api/DiagnosticsChannel.md | 3 +-- lib/web/websocket/stream/websocketstream.js | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/docs/api/DiagnosticsChannel.md b/docs/docs/api/DiagnosticsChannel.md index 30ede81a127..a04ec0e9da4 100644 --- a/docs/docs/api/DiagnosticsChannel.md +++ b/docs/docs/api/DiagnosticsChannel.md @@ -248,9 +248,8 @@ This message is published if the socket experiences an error. ```js import diagnosticsChannel from 'diagnostics_channel' -diagnosticsChannel.channel('undici:websocket:socket_error').subscribe(({ error, websocket }) => { +diagnosticsChannel.channel('undici:websocket:socket_error').subscribe((error) => { console.log(error) - console.log(websocket) // the WebSocket instance, if available }) ``` diff --git a/lib/web/websocket/stream/websocketstream.js b/lib/web/websocket/stream/websocketstream.js index dc0b053edee..ce3be84fc3d 100644 --- a/lib/web/websocket/stream/websocketstream.js +++ b/lib/web/websocket/stream/websocketstream.js @@ -57,10 +57,7 @@ class WebSocketStream { this.#handler.readyState = states.CLOSING if (channels.socketError.hasSubscribers) { - channels.socketError.publish({ - error: err, - websocket: undefined - }) + channels.socketError.publish(err) } this.#handler.socket.destroy() From 99d34aa15c7e9eafd325b50e57123bfd5844b329 Mon Sep 17 00:00:00 2001 From: ryuhei shima Date: Sat, 21 Mar 2026 21:44:52 +0900 Subject: [PATCH 06/14] unify failWebsocketConnection and remove mask from frameSent --- lib/web/websocket/receiver.js | 45 ++++++++++++++--------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/lib/web/websocket/receiver.js b/lib/web/websocket/receiver.js index 4990b63d8cd..997c1d969d0 100644 --- a/lib/web/websocket/receiver.js +++ b/lib/web/websocket/receiver.js @@ -98,14 +98,12 @@ class ByteParser extends Writable { const rsv3 = buffer[0] & 0x10 if (!isValidOpcode(opcode)) { - this.publishFrameError(new Error('Invalid opcode received')) - failWebsocketConnection(this.#handler, 1002, 'Invalid opcode received') + this.failWebsocketConnection(1002, 'Invalid opcode received') return callback() } if (masked) { - this.publishFrameError(new Error('Frame cannot be masked')) - failWebsocketConnection(this.#handler, 1002, 'Frame cannot be masked') + this.failWebsocketConnection(1002, 'Frame cannot be masked') return callback() } @@ -119,50 +117,43 @@ class ByteParser extends Writable { // WebSocket connection where a PMCE is in use, this bit indicates // whether a message is compressed or not. if (rsv1 !== 0 && !this.#extensions.has('permessage-deflate')) { - this.publishFrameError(new Error('Expected RSV1 to be clear.')) - failWebsocketConnection(this.#handler, 1002, 'Expected RSV1 to be clear.') + this.failWebsocketConnection(1002, 'Expected RSV1 to be clear.') return } if (rsv2 !== 0 || rsv3 !== 0) { - this.publishFrameError(new Error('RSV1, RSV2, RSV3 must be clear')) - failWebsocketConnection(this.#handler, 1002, 'RSV1, RSV2, RSV3 must be clear') + this.failWebsocketConnection(1002, 'RSV1, RSV2, RSV3 must be clear') return } if (fragmented && !isTextBinaryFrame(opcode)) { // Only text and binary frames can be fragmented - this.publishFrameError(new Error('Invalid frame type was fragmented.')) - failWebsocketConnection(this.#handler, 1002, 'Invalid frame type was fragmented.') + this.failWebsocketConnection(1002, 'Invalid frame type was fragmented.') return } // If we are already parsing a text/binary frame and do not receive either // a continuation frame or close frame, fail the connection. if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) { - this.publishFrameError(new Error('Expected continuation frame')) - failWebsocketConnection(this.#handler, 1002, 'Expected continuation frame') + this.failWebsocketConnection(1002, 'Expected continuation frame') return } if (this.#info.fragmented && fragmented) { // A fragmented frame can't be fragmented itself - this.publishFrameError(new Error('Fragmented frame exceeded 125 bytes.')) - failWebsocketConnection(this.#handler, 1002, 'Fragmented frame exceeded 125 bytes.') + this.failWebsocketConnection(1002, 'Fragmented frame exceeded 125 bytes.') return } // "All control frames MUST have a payload length of 125 bytes or less // and MUST NOT be fragmented." if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) { - this.publishFrameError(new Error('Control frame either too large or fragmented')) - failWebsocketConnection(this.#handler, 1002, 'Control frame either too large or fragmented') + this.failWebsocketConnection(1002, 'Control frame either too large or fragmented') return } if (isContinuationFrame(opcode) && this.#fragments.length === 0 && !this.#info.compressed) { - this.publishFrameError(new Error('Unexpected continuation frame')) - failWebsocketConnection(this.#handler, 1002, 'Unexpected continuation frame') + this.failWebsocketConnection(1002, 'Unexpected continuation frame') return } @@ -209,8 +200,7 @@ class ByteParser extends Writable { // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/common/globals.h;drc=1946212ac0100668f14eb9e2843bdd846e510a1e;bpv=1;bpt=1;l=1275 // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-array-buffer.h;l=34;drc=1946212ac0100668f14eb9e2843bdd846e510a1e if (upper !== 0 || lower > 2 ** 31 - 1) { - this.publishFrameError(new Error('Received payload length > 2^31 bytes.')) - failWebsocketConnection(this.#handler, 1009, 'Received payload length > 2^31 bytes.') + this.failWebsocketConnection(1009, 'Received payload length > 2^31 bytes.') return } @@ -227,7 +217,6 @@ class ByteParser extends Writable { channels.frameReceived.publish({ websocket: this.#handler.websocket, opcode: this.#info.opcode, - mask: this.#info.masked, payloadData: Buffer.from(body) }) } @@ -251,9 +240,8 @@ class ByteParser extends Writable { } else { this.#extensions.get('permessage-deflate').decompress(body, this.#info.fin, (error, data) => { if (error) { - this.publishFrameError(error) const code = error instanceof MessageSizeExceededError ? 1009 : 1007 - failWebsocketConnection(this.#handler, code, error.message) + this.failWebsocketConnection(code, error.message, error) return } @@ -404,8 +392,7 @@ class ByteParser extends Writable { if (opcode === opcodes.CLOSE) { if (payloadLength === 1) { - this.publishFrameError(new Error('Received close frame with a 1-byte body.')) - failWebsocketConnection(this.#handler, 1002, 'Received close frame with a 1-byte body.') + this.failWebsocketConnection(1002, 'Received close frame with a 1-byte body.') return false } @@ -414,8 +401,7 @@ class ByteParser extends Writable { if (this.#info.closeInfo.error) { const { code, reason } = this.#info.closeInfo - this.publishFrameError(new Error(reason)) - failWebsocketConnection(this.#handler, code, reason) + this.failWebsocketConnection(code, reason) return false } @@ -481,6 +467,11 @@ class ByteParser extends Writable { error }) } + + failWebsocketConnection (code, reason, error = new Error(reason)) { + this.publishFrameError(error) + failWebsocketConnection(this.#handler, code, reason) + } } module.exports = { From 30d5a53d8c9f3b70c969f8ed7e006f6d1bb03415 Mon Sep 17 00:00:00 2001 From: ryuhei shima Date: Sat, 21 Mar 2026 21:55:42 +0900 Subject: [PATCH 07/14] remove mask from frameSent --- lib/web/websocket/sender.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/web/websocket/sender.js b/lib/web/websocket/sender.js index ba2670744f9..7b7d9409760 100644 --- a/lib/web/websocket/sender.js +++ b/lib/web/websocket/sender.js @@ -40,7 +40,7 @@ class SendQueue { // TODO(@tsctx): support fast-path for string on running if (hint === sendHints.text) { // special fast-path for string - publishFrame(this.#websocket, opcodes.TEXT, true, item, hint) + publishFrame(this.#websocket, opcodes.TEXT, item, hint) const { 0: head, 1: body } = WebsocketFrameSend.createFastTextFrame(item) this.#socket.cork() this.#socket.write(head) @@ -48,7 +48,7 @@ class SendQueue { this.#socket.uncork() } else { // direct writing - publishFrame(this.#websocket, opcodes.BINARY, true, item, hint) + publishFrame(this.#websocket, opcodes.BINARY, item, hint) this.#socket.write(createFrame(item, hint), cb) } } else { @@ -72,7 +72,7 @@ class SendQueue { const node = { promise: item.arrayBuffer().then((ab) => { node.promise = null - publishFrame(this.#websocket, opcodes.BINARY, true, ab, hint) + publishFrame(this.#websocket, opcodes.BINARY, ab, hint) node.diagnosticInfo.published = true node.frame = createFrame(ab, hint) }), @@ -129,7 +129,7 @@ function toBuffer (data, hint) { } } -function publishFrame (websocket, opcode, mask, data, hint) { +function publishFrame (websocket, opcode, data, hint) { if (!channels.frameSent.hasSubscribers) { return } @@ -137,7 +137,6 @@ function publishFrame (websocket, opcode, mask, data, hint) { channels.frameSent.publish({ websocket, opcode, - mask, payloadData: Buffer.from(toBuffer(data, hint)) }) } @@ -150,7 +149,6 @@ function publishQueuedFrame (websocket, frame, diagnosticInfo) { channels.frameSent.publish({ websocket, opcode: frame[0] & 0x0F, - mask: (frame[1] & 0x80) === 0x80, payloadData: Buffer.from(toBuffer(diagnosticInfo.data, diagnosticInfo.hint)) }) } From 0b440301aac8c3ddb675d8d136cd1e2213465397 Mon Sep 17 00:00:00 2001 From: ryuhei shima Date: Sat, 21 Mar 2026 21:57:25 +0900 Subject: [PATCH 08/14] adjust the timing of publishFrame --- lib/web/websocket/sender.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/web/websocket/sender.js b/lib/web/websocket/sender.js index 7b7d9409760..8fd00eeabf1 100644 --- a/lib/web/websocket/sender.js +++ b/lib/web/websocket/sender.js @@ -10,7 +10,7 @@ const { channels } = require('../../core/diagnostics') * @property {Promise | null} promise * @property {((...args: any[]) => any)} callback * @property {Buffer | null} frame - * @property {{ published: boolean, data: Buffer | ArrayBuffer | ArrayBufferView | null, hint: number }} diagnosticInfo + * @property {{ data: Buffer | ArrayBuffer | ArrayBufferView | null, hint: number }} diagnosticInfo */ class SendQueue { @@ -58,7 +58,6 @@ class SendQueue { callback: cb, frame: createFrame(item, hint), diagnosticInfo: { - published: false, data: item, hint } @@ -72,14 +71,12 @@ class SendQueue { const node = { promise: item.arrayBuffer().then((ab) => { node.promise = null - publishFrame(this.#websocket, opcodes.BINARY, ab, hint) - node.diagnosticInfo.published = true + node.diagnosticInfo.data = ab node.frame = createFrame(ab, hint) }), callback: cb, frame: null, diagnosticInfo: { - published: false, data: item, hint } @@ -102,9 +99,8 @@ class SendQueue { await node.promise } // write - if (node.frame !== null && !node.diagnosticInfo.published) { + if (node.frame !== null) { publishQueuedFrame(this.#websocket, node.frame, node.diagnosticInfo) - node.diagnosticInfo.published = true } this.#socket.write(node.frame, node.callback) // cleanup From 98c94299867bd5d0f43a3cd545b7edb8db6efedc Mon Sep 17 00:00:00 2001 From: ryuhei shima Date: Sat, 21 Mar 2026 22:34:02 +0900 Subject: [PATCH 09/14] fix test --- test/websocket/diagnostics-channel-frames.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/websocket/diagnostics-channel-frames.js b/test/websocket/diagnostics-channel-frames.js index 4563e8e6e19..fab953b5382 100644 --- a/test/websocket/diagnostics-channel-frames.js +++ b/test/websocket/diagnostics-channel-frames.js @@ -8,7 +8,7 @@ const { WebSocket } = require('../..') const { opcodes } = require('../../lib/web/websocket/constants') test('diagnostics channel - undici:websocket:[frameSent/frameReceived]', async (t) => { - t.plan(8) + t.plan(6) const server = new WebSocketServer({ port: 0 }) const ws = new WebSocket(`ws://localhost:${server.address().port}`) @@ -26,7 +26,6 @@ test('diagnostics channel - undici:websocket:[frameSent/frameReceived]', async ( t.assert.strictEqual(websocket, ws) t.assert.strictEqual(opcode, opcodes.TEXT) - t.assert.strictEqual(mask, true) t.assert.strictEqual(payloadData.toString(), 'hello') } @@ -37,7 +36,6 @@ test('diagnostics channel - undici:websocket:[frameSent/frameReceived]', async ( t.assert.strictEqual(websocket, ws) t.assert.strictEqual(opcode, opcodes.TEXT) - t.assert.strictEqual(mask, false) t.assert.strictEqual(payloadData.toString(), 'hello') } From ed80a363cba1362f73191e8b1a630a5a3ba7eb0d Mon Sep 17 00:00:00 2001 From: ryuhei shima Date: Sat, 21 Mar 2026 22:50:18 +0900 Subject: [PATCH 10/14] remove websocket from socket_error --- lib/web/websocket/websocket.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index 7fb3d3e4408..e7c0d31933a 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -76,10 +76,7 @@ class WebSocket extends EventTarget { this.#handler.readyState = states.CLOSING if (channels.socketError.hasSubscribers) { - channels.socketError.publish({ - error: err, - websocket: this - }) + channels.socketError.publish(err) } this.#handler.socket.destroy() From 0419a1be7cd8b63b1f7d7c9204820456b2864848 Mon Sep 17 00:00:00 2001 From: ryuhei shima Date: Sat, 21 Mar 2026 23:01:43 +0900 Subject: [PATCH 11/14] remove websocketError type --- test/types/diagnostics-channel.test-d.ts | 4 ---- types/diagnostics-channel.d.ts | 4 ---- 2 files changed, 8 deletions(-) diff --git a/test/types/diagnostics-channel.test-d.ts b/test/types/diagnostics-channel.test-d.ts index f35411139f1..6d84276d74d 100644 --- a/test/types/diagnostics-channel.test-d.ts +++ b/test/types/diagnostics-channel.test-d.ts @@ -115,7 +115,3 @@ expectAssignable({ websocket, error: new Error('Error') }) -expectAssignable({ - websocket, - error: new Error('Error') -}) diff --git a/types/diagnostics-channel.d.ts b/types/diagnostics-channel.d.ts index e149e76054b..687c79f372d 100644 --- a/types/diagnostics-channel.d.ts +++ b/types/diagnostics-channel.d.ts @@ -112,8 +112,4 @@ declare namespace DiagnosticsChannel { websocket: WebSocket; error: Error; } - export interface WebsocketSocketErrorMessage { - websocket?: WebSocket; - error: Error; - } } From 805449c0831424df50c119917b9cdba71630b925 Mon Sep 17 00:00:00 2001 From: Ryuhei Shima <65934663+islandryu@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:21:15 +0900 Subject: [PATCH 12/14] Update docs/docs/api/DiagnosticsChannel.md Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> --- docs/docs/api/DiagnosticsChannel.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/docs/api/DiagnosticsChannel.md b/docs/docs/api/DiagnosticsChannel.md index a04ec0e9da4..da5ab1df21e 100644 --- a/docs/docs/api/DiagnosticsChannel.md +++ b/docs/docs/api/DiagnosticsChannel.md @@ -260,10 +260,9 @@ This message is published after a WebSocket frame is written to the socket. ```js import diagnosticsChannel from 'diagnostics_channel' -diagnosticsChannel.channel('undici:websocket:frameSent').subscribe(({ websocket, opcode, mask, payloadData }) => { +diagnosticsChannel.channel('undici:websocket:frameSent').subscribe(({ websocket, opcode, payloadData }) => { console.log(websocket) // the WebSocket instance console.log(opcode) // RFC 6455 opcode - console.log(mask) // true for client-sent frames console.log(payloadData) // unmasked payload bytes }) ``` From 381a54fb3b500f6274698bd0ab3ccc646f6f15a9 Mon Sep 17 00:00:00 2001 From: Ryuhei Shima <65934663+islandryu@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:21:25 +0900 Subject: [PATCH 13/14] Update docs/docs/api/DiagnosticsChannel.md Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> --- docs/docs/api/DiagnosticsChannel.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/docs/api/DiagnosticsChannel.md b/docs/docs/api/DiagnosticsChannel.md index da5ab1df21e..a18173fd309 100644 --- a/docs/docs/api/DiagnosticsChannel.md +++ b/docs/docs/api/DiagnosticsChannel.md @@ -274,10 +274,9 @@ This message is published after a WebSocket frame is parsed from the socket. ```js import diagnosticsChannel from 'diagnostics_channel' -diagnosticsChannel.channel('undici:websocket:frameReceived').subscribe(({ websocket, opcode, mask, payloadData }) => { +diagnosticsChannel.channel('undici:websocket:frameReceived').subscribe(({ websocket, opcode, payloadData }) => { console.log(websocket) // the WebSocket instance console.log(opcode) // RFC 6455 opcode - console.log(mask) // false for server-sent frames console.log(payloadData) // payload bytes as received }) ``` From 69af2db18dfdcfb0c0d20bf06affec6794d446aa Mon Sep 17 00:00:00 2001 From: Ryuhei Shima <65934663+islandryu@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:22:08 +0900 Subject: [PATCH 14/14] Update lib/core/diagnostics.js Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> --- lib/core/diagnostics.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/diagnostics.js b/lib/core/diagnostics.js index c4697625d50..f63828077e4 100644 --- a/lib/core/diagnostics.js +++ b/lib/core/diagnostics.js @@ -227,8 +227,8 @@ function trackWebSocketEvents (debugLog = websocketDebuglog) { }) diagnosticsChannel.subscribe('undici:websocket:socket_error', - evt => { - debugLog('connection errored for %s - %s', evt.websocket?.url ?? '', evt.error.message) + err => { + debugLog('connection errored - %s', err.message) }) diagnosticsChannel.subscribe('undici:websocket:ping',