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/lib/web/websocket/websocket.js b/lib/web/websocket/websocket.js index 5688c8ad8a0..32e716c489d 100644 --- a/lib/web/websocket/websocket.js +++ b/lib/web/websocket/websocket.js @@ -482,11 +482,18 @@ class WebSocket extends EventTarget { fireEvent('open', this) if (channels.open.hasSubscribers) { + // Convert headers to a plain object for the event + const headers = response.headersList.entries 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..8ca79cfa17d --- /dev/null +++ b/test/websocket/diagnostics-channel-handshake-response.js @@ -0,0 +1,51 @@ +'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: 11 }) + + const server = new WebSocketServer({ port: 0 }) + const { port } = server.address() + + server.on('connection', (ws) => { + setTimeout(() => { + ws.close(1000, 'test') + }, 50) + }) + + 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') + // 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.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) + + t.after(() => { + server.close() + dc.channel('undici:websocket:open').unsubscribe(openListener) + }) + + // Create WebSocket connection + // eslint-disable-next-line no-unused-vars + const _ws = new WebSocket(`ws://localhost:${port}`) + + await completed +})