Repository: nodejs/undici (JavaScript)

Issue:
Add handshake response info to undici:websocket:open diagnostic event
## This would solve...

<!-- A clear and concise description of the problem this feature request relates
to, if applicable. -->

Network.webSocketHandshakeResponseReceived in the Chrome DevTools Protocol (CDP), we need access to the handshake's HTTP response details (status, statusText, headers) at the time the connection is established.
https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-webSocketHandshakeResponseReceived
## The implementation should look like...

<!-- A clear and concise description of how you expect this to be resolved or
implemented. -->
Add this field to `undici:websocket:open`
```
{
  handshakeResponse: {
    status: 101,
    statusText: 'Switching Protocols',
    headers: { Upgrade: 'websocket', Connection: 'Upgrade', ... }
  }
}
```
## I have also considered...
Create a dedicated handshake-specific event, similar to CDP.


<!-- A clear and concise description of any alternative solutions or features
you have considered. -->

## Additional context

<!-- Add any other context, screenshots or ideas about the feature request
here. -->
I am adding WebSocket inspector support in the following PR.
https://github.com/nodejs/node/pull/59404


Discussion:

Patch:
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
+        }
       })
     }
   }


Test patch:
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
+})