From 2f87dbff6b5fbf94ec6862f2f2fb091603a50f43 Mon Sep 17 00:00:00 2001 From: tawseefnabi Date: Fri, 8 Aug 2025 17:00:27 +0530 Subject: [PATCH 1/6] feat(websocket): add handshake response info to undici:websocket:open diagnostic event - Add handshakeResponse object to undici:websocket:open diagnostic event - Include status, statusText, and headers from HTTP handshake response - Enables Chrome DevTools Protocol Network.webSocketHandshakeResponseReceived support - Add comprehensive test coverage for handshake response diagnostic data Fixes #4394 --- lib/web/websocket/websocket.js | 13 +++++- .../diagnostics-channel-handshake-response.js | 40 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 test/websocket/diagnostics-channel-handshake-response.js diff --git a/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index 5688c8ad8a0..37773656bf4 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -482,11 +482,22 @@ class WebSocket extends EventTarget { fireEvent('open', this) if (channels.open.hasSubscribers) { + // Convert headers to a plain object for the event + const headers = {} + for (const [key, value] of response.headersList) { + headers[key] = value + } + channels.open.publish({ address: response.socket.address(), protocol: this.#protocol, extensions: this.#extensions, - websocket: this + websocket: this, + handshakeResponse: { + status: response.status, + statusText: response.statusText, + headers + } }) } } diff --git a/test/websocket/diagnostics-channel-handshake-response.js b/test/websocket/diagnostics-channel-handshake-response.js new file mode 100644 index 00000000000..c933e689cd9 --- /dev/null +++ b/test/websocket/diagnostics-channel-handshake-response.js @@ -0,0 +1,40 @@ +'use strict' + +const { test } = require('node:test') +const dc = require('node:diagnostics_channel') +const { WebSocketServer } = require('ws') +const { WebSocket } = require('../..') +const { tspl } = require('@matteo.collina/tspl') + +test('diagnostics channel - undici:websocket:open includes handshake response', async (t) => { + const { equal, ok, completed } = tspl(t, { plan: 3 }) + + const server = new WebSocketServer({ port: 0 }) + const { port } = server.address() + + server.on('connection', (ws) => { + ws.close(1000, 'test') + }) + + const openListener = (data) => { + // Verify handshake response data + ok(data.handshakeResponse, 'handshakeResponse should be defined') + equal(data.handshakeResponse.status, 101, 'status should be 101') + equal(data.handshakeResponse.statusText, 'Switching Protocols', 'statusText should be correct') + } + + dc.channel('undici:websocket:open').subscribe(openListener) + + t.after(() => { + server.close() + dc.channel('undici:websocket:open').unsubscribe(openListener) + }) + + // Create WebSocket connection + const ws = new WebSocket(`ws://localhost:${port}`) + + // Ensure connection is created (avoid no-new linting error) + ws.url // eslint-disable-line no-unused-expressions + + await completed +}) From 5f68c38974a52f00c7eb39bb58c53b62792a69ed Mon Sep 17 00:00:00 2001 From: tawseefnabi Date: Fri, 8 Aug 2025 20:35:36 +0530 Subject: [PATCH 2/6] fix(websocket/diagnostics): use headersList.headersMap.entries() for diagnostics channel event emission - Fixes diagnostics channel WebSocket event emission and related test timeouts in Node.js 20+. - Cleans up debug/test scripts and logs. --- lib/web/websocket/websocket.js | 40 ++++++++++++------- .../diagnostics-channel-handshake-response.js | 10 ++++- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index 37773656bf4..7ad0a84da65 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -483,22 +483,32 @@ class WebSocket extends EventTarget { if (channels.open.hasSubscribers) { // Convert headers to a plain object for the event - const headers = {} - for (const [key, value] of response.headersList) { - headers[key] = value - } + const headers = Object.fromEntries(response.headersList.headersMap.entries()) + if (channels.open.hasSubscribers) { + console.log('[IMPL] About to publish undici:websocket:open event', { + address: response.socket.address(), + protocol: this.#protocol, + extensions: this.#extensions, + handshakeResponse: { + status: response.status, + statusText: response.statusText, + headers + } + }) - channels.open.publish({ - address: response.socket.address(), - protocol: this.#protocol, - extensions: this.#extensions, - websocket: this, - handshakeResponse: { - status: response.status, - statusText: response.statusText, - headers - } - }) + channels.open.publish({ + address: response.socket.address(), + protocol: this.#protocol, + extensions: this.#extensions, + websocket: this, + handshakeResponse: { + status: response.status, + statusText: response.statusText, + headers + } + }) + console.log('[IMPL] Published undici:websocket:open event') + } } } diff --git a/test/websocket/diagnostics-channel-handshake-response.js b/test/websocket/diagnostics-channel-handshake-response.js index c933e689cd9..4420d656554 100644 --- a/test/websocket/diagnostics-channel-handshake-response.js +++ b/test/websocket/diagnostics-channel-handshake-response.js @@ -11,12 +11,18 @@ test('diagnostics channel - undici:websocket:open includes handshake response', const server = new WebSocketServer({ port: 0 }) const { port } = server.address() + console.log('[TEST] WebSocketServer started on port', port) server.on('connection', (ws) => { - ws.close(1000, 'test') + console.log('[TEST] Server: connection established') + setTimeout(() => { + ws.close(1000, 'test') + console.log('[TEST] Server: connection closed') + }, 50) }) const openListener = (data) => { + console.log('[TEST] openListener called:', data) // Verify handshake response data ok(data.handshakeResponse, 'handshakeResponse should be defined') equal(data.handshakeResponse.status, 101, 'status should be 101') @@ -28,10 +34,12 @@ test('diagnostics channel - undici:websocket:open includes handshake response', t.after(() => { server.close() dc.channel('undici:websocket:open').unsubscribe(openListener) + console.log('[TEST] Cleanup complete') }) // Create WebSocket connection const ws = new WebSocket(`ws://localhost:${port}`) + console.log('[TEST] WebSocket client created:', ws.url) // Ensure connection is created (avoid no-new linting error) ws.url // eslint-disable-line no-unused-expressions From 92f6fcca0dee596d8df0e883705fc495466de990 Mon Sep 17 00:00:00 2001 From: tawseefnabi Date: Fri, 8 Aug 2025 21:32:58 +0530 Subject: [PATCH 3/6] fix(websocket/diagnostics): clean up after review, remove debug logs, redundant checks, and improve handshake header assertions in test --- lib/web/websocket/websocket.js | 36 ++++++------------- .../diagnostics-channel-handshake-response.js | 14 ++++++-- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index 7ad0a84da65..3729bbc3817 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -484,31 +484,17 @@ class WebSocket extends EventTarget { if (channels.open.hasSubscribers) { // Convert headers to a plain object for the event const headers = Object.fromEntries(response.headersList.headersMap.entries()) - if (channels.open.hasSubscribers) { - console.log('[IMPL] About to publish undici:websocket:open event', { - address: response.socket.address(), - protocol: this.#protocol, - extensions: this.#extensions, - handshakeResponse: { - status: response.status, - statusText: response.statusText, - headers - } - }) - - channels.open.publish({ - address: response.socket.address(), - protocol: this.#protocol, - extensions: this.#extensions, - websocket: this, - handshakeResponse: { - status: response.status, - statusText: response.statusText, - headers - } - }) - console.log('[IMPL] Published undici:websocket:open event') - } + channels.open.publish({ + address: response.socket.address(), + protocol: this.#protocol, + extensions: this.#extensions, + websocket: this, + handshakeResponse: { + status: response.status, + statusText: response.statusText, + headers + } + }) } } diff --git a/test/websocket/diagnostics-channel-handshake-response.js b/test/websocket/diagnostics-channel-handshake-response.js index 4420d656554..ca3cb55a5ac 100644 --- a/test/websocket/diagnostics-channel-handshake-response.js +++ b/test/websocket/diagnostics-channel-handshake-response.js @@ -27,6 +27,17 @@ test('diagnostics channel - undici:websocket:open includes handshake response', ok(data.handshakeResponse, 'handshakeResponse should be defined') equal(data.handshakeResponse.status, 101, 'status should be 101') equal(data.handshakeResponse.statusText, 'Switching Protocols', 'statusText should be correct') + // Check handshake response headers + const headers = data.handshakeResponse.headers + ok(headers, 'headers should be defined') + ok(typeof headers === 'object', 'headers should be an object') + ok('upgrade' in headers, 'upgrade header should be present') + ok('connection' in headers, 'connection header should be present') + ok('sec-websocket-accept' in headers, 'sec-websocket-accept header should be present') + // Optionally, check values + equal(headers.upgrade.value.toLowerCase(), 'websocket', 'upgrade header should be websocket') + equal(headers.connection.value.toLowerCase(), 'upgrade', 'connection header should be upgrade') + ok(typeof headers['sec-websocket-accept'].value === 'string', 'sec-websocket-accept header should be a string') } dc.channel('undici:websocket:open').subscribe(openListener) @@ -41,8 +52,5 @@ test('diagnostics channel - undici:websocket:open includes handshake response', const ws = new WebSocket(`ws://localhost:${port}`) console.log('[TEST] WebSocket client created:', ws.url) - // Ensure connection is created (avoid no-new linting error) - ws.url // eslint-disable-line no-unused-expressions - await completed }) From 766e853c34eb87012cca66a0f6e427199e999282 Mon Sep 17 00:00:00 2001 From: tawseefnabi Date: Sat, 9 Aug 2025 06:54:03 +0530 Subject: [PATCH 4/6] refactor: use HeadersList.entries getter for WebSocket diagnostic headers - Replace Object.fromEntries(response.headersList.headersMap.entries()) with response.headersList.entries - Use the built-in getter method from HeadersList class for cleaner, more efficient code - Update test to match new plain object format (remove .value property access) - Fix test plan count to match actual number of assertions (11) Addresses KhafraDev's code review feedback to use the purpose-built entries getter. --- lib/web/websocket/websocket.js | 2 +- test/websocket/diagnostics-channel-handshake-response.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index 3729bbc3817..32e716c489d 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -483,7 +483,7 @@ class WebSocket extends EventTarget { if (channels.open.hasSubscribers) { // Convert headers to a plain object for the event - const headers = Object.fromEntries(response.headersList.headersMap.entries()) + const headers = response.headersList.entries channels.open.publish({ address: response.socket.address(), protocol: this.#protocol, diff --git a/test/websocket/diagnostics-channel-handshake-response.js b/test/websocket/diagnostics-channel-handshake-response.js index ca3cb55a5ac..948b0f6882c 100644 --- a/test/websocket/diagnostics-channel-handshake-response.js +++ b/test/websocket/diagnostics-channel-handshake-response.js @@ -7,7 +7,7 @@ const { WebSocket } = require('../..') const { tspl } = require('@matteo.collina/tspl') test('diagnostics channel - undici:websocket:open includes handshake response', async (t) => { - const { equal, ok, completed } = tspl(t, { plan: 3 }) + const { equal, ok, completed } = tspl(t, { plan: 11 }) const server = new WebSocketServer({ port: 0 }) const { port } = server.address() @@ -35,9 +35,9 @@ test('diagnostics channel - undici:websocket:open includes handshake response', ok('connection' in headers, 'connection header should be present') ok('sec-websocket-accept' in headers, 'sec-websocket-accept header should be present') // Optionally, check values - equal(headers.upgrade.value.toLowerCase(), 'websocket', 'upgrade header should be websocket') - equal(headers.connection.value.toLowerCase(), 'upgrade', 'connection header should be upgrade') - ok(typeof headers['sec-websocket-accept'].value === 'string', 'sec-websocket-accept header should be a string') + equal(headers.upgrade.toLowerCase(), 'websocket', 'upgrade header should be websocket') + equal(headers.connection.toLowerCase(), 'upgrade', 'connection header should be upgrade') + ok(typeof headers['sec-websocket-accept'] === 'string', 'sec-websocket-accept header should be a string') } dc.channel('undici:websocket:open').subscribe(openListener) From 814e102665fe8e09a024ab667db82a1c05cb0c10 Mon Sep 17 00:00:00 2001 From: tawseefnabi Date: Sat, 9 Aug 2025 13:07:25 +0530 Subject: [PATCH 5/6] docs: document handshakeResponse in websocket:open diagnostics - Add handshakeResponse object documentation to DiagnosticsChannel.md - Remove console.log statements from test file - Fix linting issues in test file --- docs/docs/api/DiagnosticsChannel.md | 26 ++++++++++++++++++- .../diagnostics-channel-handshake-response.js | 5 ---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/docs/api/DiagnosticsChannel.md b/docs/docs/api/DiagnosticsChannel.md index dab13a7df6e..096bd58ce29 100644 --- a/docs/docs/api/DiagnosticsChannel.md +++ b/docs/docs/api/DiagnosticsChannel.md @@ -169,14 +169,38 @@ This message is published after the client has successfully connected to a serve ```js import diagnosticsChannel from 'diagnostics_channel' -diagnosticsChannel.channel('undici:websocket:open').subscribe(({ address, protocol, extensions, websocket }) => { +diagnosticsChannel.channel('undici:websocket:open').subscribe(({ + address, // { address: string, family: string, port: number } + protocol, // string - negotiated subprotocol + extensions, // string - negotiated extensions + websocket, // WebSocket - the WebSocket instance + handshakeResponse // object - HTTP response that upgraded the connection +}) => { console.log(address) // address, family, and port console.log(protocol) // negotiated subprotocols console.log(extensions) // negotiated extensions console.log(websocket) // the WebSocket instance + + // Handshake response details + console.log(handshakeResponse.status) // 101 for successful WebSocket upgrade + console.log(handshakeResponse.statusText) // 'Switching Protocols' + console.log(handshakeResponse.headers) // Object containing response headers }) ``` +### Handshake Response Object + +The `handshakeResponse` object contains the HTTP response that upgraded the connection to WebSocket: + +- `status` (number): The HTTP status code (101 for successful WebSocket upgrade) +- `statusText` (string): The HTTP status message ('Switching Protocols' for successful upgrade) +- `headers` (object): The HTTP response headers from the server, including: + - `upgrade: 'websocket'` + - `connection: 'upgrade'` + - `sec-websocket-accept` and other WebSocket-related headers + +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:close` This message is published after the connection has closed. diff --git a/test/websocket/diagnostics-channel-handshake-response.js b/test/websocket/diagnostics-channel-handshake-response.js index 948b0f6882c..f4b5890193c 100644 --- a/test/websocket/diagnostics-channel-handshake-response.js +++ b/test/websocket/diagnostics-channel-handshake-response.js @@ -11,18 +11,14 @@ test('diagnostics channel - undici:websocket:open includes handshake response', const server = new WebSocketServer({ port: 0 }) const { port } = server.address() - console.log('[TEST] WebSocketServer started on port', port) server.on('connection', (ws) => { - console.log('[TEST] Server: connection established') setTimeout(() => { ws.close(1000, 'test') - console.log('[TEST] Server: connection closed') }, 50) }) const openListener = (data) => { - console.log('[TEST] openListener called:', data) // Verify handshake response data ok(data.handshakeResponse, 'handshakeResponse should be defined') equal(data.handshakeResponse.status, 101, 'status should be 101') @@ -45,7 +41,6 @@ test('diagnostics channel - undici:websocket:open includes handshake response', t.after(() => { server.close() dc.channel('undici:websocket:open').unsubscribe(openListener) - console.log('[TEST] Cleanup complete') }) // Create WebSocket connection From e62a78aab35b1ce1fe60655cbd5ec2762e449b78 Mon Sep 17 00:00:00 2001 From: tawseefnabi Date: Sat, 9 Aug 2025 13:11:18 +0530 Subject: [PATCH 6/6] test: remove console.log and mark unused WebSocket variable --- test/websocket/diagnostics-channel-handshake-response.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/websocket/diagnostics-channel-handshake-response.js b/test/websocket/diagnostics-channel-handshake-response.js index f4b5890193c..8ca79cfa17d 100644 --- a/test/websocket/diagnostics-channel-handshake-response.js +++ b/test/websocket/diagnostics-channel-handshake-response.js @@ -44,8 +44,8 @@ test('diagnostics channel - undici:websocket:open includes handshake response', }) // Create WebSocket connection - const ws = new WebSocket(`ws://localhost:${port}`) - console.log('[TEST] WebSocket client created:', ws.url) + // eslint-disable-next-line no-unused-vars + const _ws = new WebSocket(`ws://localhost:${port}`) await completed })