Repository: nodejs/undici (JavaScript)

Issue:
bug: debug logs duplicated
## Bug Description

When launching debug mode, all events are written to logs 2-3 times.
Moreover, if you subscribe to the channel yourself, you will receive one event.

I understood that this is due to the fact that we subscribe to the event twice if we use undici, this is in undici inside the node and in undici as a dependency.

## Reproducible By

```bash
NODE_DEBUG=undici node ./debug.js
```

```ts
import { request, Agent } from 'undici';

const dispatcher = new Agent({ connections: 1 });

async function execute() {
  const resp = await request('https://cdn.datatables.net/2.3.4/js/dataTables.min.js', { dispatcher });
  await resp.body.dump();
}

execute();
```

```ts
async function undici() {
  const { fetch} = await import('undici');
  const resp = await fetch('https://cdn.datatables.net/2.3.4/js/dataTables.min.js');
  await resp.arrayBuffer();
}

async function nodeUndici(){
  const resp = await fetch('https://cdn.datatables.net/2.3.4/js/dataTables.min.js');
  await resp.arrayBuffer();
}

// one debug logs
nodeUndici();

// duplicated debug logs
// undici();
```

## Expected Behavior

Logs for a single query should not be duplicated.

## Logs & Screenshots

<img width="797" height="190" alt="Image" src="https://github.com/user-attachments/assets/eba3f142-9b3f-4a43-8f8b-65d86622b447" />

## Environment

node - v24.9.0 and v22.19.0
undici - 7.16.0

### Additional context

<!-- Add any other context about the problem here. -->



Discussion:
Yes, this is true. This is because of the use of the diagnostics channel. 
The duplicated logs also has bad format. 

I need to review this a bit deeper, but happy to review a PR for that.



Patch:
diff --git a/lib/core/diagnostics.js b/lib/core/diagnostics.js
index 224a5c49f5d..dd1387d5132 100644
--- a/lib/core/diagnostics.js
+++ b/lib/core/diagnostics.js
@@ -36,6 +36,14 @@ function trackClientEvents (debugLog = undiciDebugLog) {
     return
   }
 
+  // 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.beforeConnect.hasSubscribers || channels.connected.hasSubscribers ||
+      channels.connectError.hasSubscribers || channels.sendHeaders.hasSubscribers) {
+    isTrackingClientEvents = true
+    return
+  }
+
   isTrackingClientEvents = true
 
   diagnosticsChannel.subscribe('undici:client:beforeConnect',
@@ -98,6 +106,14 @@ function trackRequestEvents (debugLog = undiciDebugLog) {
     return
   }
 
+  // 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.headers.hasSubscribers || channels.trailers.hasSubscribers ||
+      channels.error.hasSubscribers) {
+    isTrackingRequestEvents = true
+    return
+  }
+
   isTrackingRequestEvents = true
 
   diagnosticsChannel.subscribe('undici:request:headers',
@@ -146,6 +162,15 @@ function trackWebSocketEvents (debugLog = websocketDebuglog) {
     return
   }
 
+  // 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) {
+    isTrackingWebSocketEvents = true
+    return
+  }
+
   isTrackingWebSocketEvents = true
 
   diagnosticsChannel.subscribe('undici:websocket:open',


Test patch:
diff --git a/test/fixtures/duplicate-debug.js b/test/fixtures/duplicate-debug.js
new file mode 100644
index 00000000000..b8735cfd98b
--- /dev/null
+++ b/test/fixtures/duplicate-debug.js
@@ -0,0 +1,24 @@
+'use strict'
+
+const { createServer } = require('node:http')
+const { request } = require('../..')
+
+// Simulate the scenario where diagnostics module is loaded multiple times
+// This mimics having both Node.js built-in undici and undici as dependency
+delete require.cache[require.resolve('../../lib/core/diagnostics.js')]
+require('../../lib/core/diagnostics.js')
+
+const server = createServer({ joinDuplicateHeaders: true }, (_req, res) => {
+  res.writeHead(200, { 'Content-Type': 'text/plain' })
+  res.end('hello world')
+})
+
+server.listen(0, () => {
+  const { port, address, family } = server.address()
+  const hostname = family === 'IPv6' ? `[${address}]` : address
+  request(`http://${hostname}:${port}`)
+    .then(res => res.body.dump())
+    .then(() => {
+      server.close()
+    })
+})
diff --git a/test/node-test/debug.js b/test/node-test/debug.js
index f85dff58cf9..2e047175307 100644
--- a/test/node-test/debug.js
+++ b/test/node-test/debug.js
@@ -126,6 +126,46 @@ test('debug#undici', { skip: isCITGM || isNode23Plus }, async t => {
   await assert.completed
 })
 
+test('debug#undici no duplicates', { skip: isCITGM || isNode23Plus }, async t => {
+  const assert = tspl(t, { plan: 7 })
+  const child = spawn(
+    process.execPath,
+    [
+      '--no-experimental-fetch',
+      join(__dirname, '../fixtures/duplicate-debug.js')
+    ],
+    {
+      env: {
+        NODE_DEBUG: 'undici'
+      }
+    }
+  )
+  const chunks = []
+  const assertions = [
+    /(UNDICI [0-9]+:) (connecting to)/,
+    /(UNDICI [0-9]+:) (connected to)/,
+    /(UNDICI [0-9]+:) (sending request)/,
+    /(UNDICI [0-9]+:) (received response)/,
+    /(UNDICI [0-9]+:) (trailers received)/,
+    /^$/
+  ]
+
+  child.stderr.setEncoding('utf8')
+  child.stderr.on('data', chunk => {
+    chunks.push(chunk)
+  })
+  child.stderr.on('end', () => {
+    const lines = extractLines(chunks)
+    // Should have exactly the expected number of lines, no duplicates
+    assert.strictEqual(lines.length, assertions.length, 'Should not have duplicate log lines')
+    for (let i = 0; i < lines.length; i++) {
+      assert.match(lines[i], assertions[i])
+    }
+  })
+
+  await assert.completed
+})
+
 function extractLines (chunks) {
   return chunks
     .join('')