diff --git a/README.md b/README.md index f3b929b0dc4..c15ffd0cc86 100644 --- a/README.md +++ b/README.md @@ -621,7 +621,7 @@ including undici that is bundled internally with node.js. Undici stores this dispatcher under `Symbol.for('undici.globalDispatcher.2')`. -On Node.js 22, `setGlobalDispatcher()` also mirrors the configured dispatcher to +`setGlobalDispatcher()` also mirrors the configured dispatcher to `Symbol.for('undici.globalDispatcher.1')` using `Dispatcher1Wrapper`, so Node.js built-in `fetch` can keep using the legacy handler contract while Undici uses the new handler API. diff --git a/docs/docs/api/Dispatcher.md b/docs/docs/api/Dispatcher.md index 666a58ff8a6..c731690bcf4 100644 --- a/docs/docs/api/Dispatcher.md +++ b/docs/docs/api/Dispatcher.md @@ -237,7 +237,7 @@ Pause/resume now uses the controller: Undici now stores the global dispatcher under `Symbol.for('undici.globalDispatcher.2')`. This avoids conflicts with runtimes (such as Node.js built-in `fetch`) that still rely on the legacy dispatcher handler interface. -On Node.js 22, `setGlobalDispatcher()` also mirrors the configured dispatcher to `Symbol.for('undici.globalDispatcher.1')` using a `Dispatcher1Wrapper`, so Node's built-in `fetch` can keep using the legacy handler contract. +`setGlobalDispatcher()` also mirrors the configured dispatcher to `Symbol.for('undici.globalDispatcher.1')` using a `Dispatcher1Wrapper`, so Node's built-in `fetch` can keep using the legacy handler contract. If you need to expose a new dispatcher/agent to legacy v1 handler consumers (`onConnect/onHeaders/onData/onComplete/onError/onUpgrade`), use `Dispatcher1Wrapper`: diff --git a/lib/global.js b/lib/global.js index f518dae3127..81ab7d10195 100644 --- a/lib/global.js +++ b/lib/global.js @@ -8,8 +8,6 @@ const { InvalidArgumentError } = require('./core/errors') const Agent = require('./dispatcher/agent') const Dispatcher1Wrapper = require('./dispatcher/dispatcher1-wrapper') -const nodeMajor = Number(process.versions.node.split('.', 1)[0]) - if (getGlobalDispatcher() === undefined) { setGlobalDispatcher(new Agent()) } @@ -26,16 +24,14 @@ function setGlobalDispatcher (agent) { configurable: false }) - if (nodeMajor === 22) { - const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent) + const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent) - Object.defineProperty(globalThis, legacyGlobalDispatcher, { - value: legacyAgent, - writable: true, - enumerable: false, - configurable: false - }) - } + Object.defineProperty(globalThis, legacyGlobalDispatcher, { + value: legacyAgent, + writable: true, + enumerable: false, + configurable: false + }) } function getGlobalDispatcher () { diff --git a/test/node-test/global-dispatcher-version.js b/test/node-test/global-dispatcher-version.js index ecd9d706804..e20a181a4dd 100644 --- a/test/node-test/global-dispatcher-version.js +++ b/test/node-test/global-dispatcher-version.js @@ -42,31 +42,59 @@ test('setGlobalDispatcher does not break Node.js global fetch', () => { assert.strictEqual(result.stdout, 'ok') }) -test('setGlobalDispatcher mirrors a v1-compatible dispatcher on Node.js 22', () => { +test('setGlobalDispatcher mirrors a v1-compatible dispatcher that Node.js global fetch uses', () => { const script = ` const { Agent, Dispatcher1Wrapper, setGlobalDispatcher } = require('./index.js') - const nodeMajor = Number(process.versions.node.split('.', 1)[0]) + const http = require('node:http') + const { once } = require('node:events') - setGlobalDispatcher(new Agent()) + ;(async () => { + const dispatcherV1Symbol = Symbol.for('undici.globalDispatcher.1') + const dispatcherV2Symbol = Symbol.for('undici.globalDispatcher.2') + const server = http.createServer((req, res) => res.end('ok')) + server.listen(0) + await once(server, 'listening') + + let count = 0 + class CountingAgent extends Agent { + dispatch (opts, handler) { + count++ + return super.dispatch(opts, handler) + } + } - if (nodeMajor !== 22) { - process.stdout.write('skipped') - } else { - const dispatcherV1 = globalThis[Symbol.for('undici.globalDispatcher.1')] + const agent = new CountingAgent() + setGlobalDispatcher(agent) + const dispatcherV1 = globalThis[dispatcherV1Symbol] if (!(dispatcherV1 instanceof Dispatcher1Wrapper)) { - throw new Error('expected v1 global dispatcher to be a Dispatcher1Wrapper on Node.js 22') + throw new Error('expected v1 global dispatcher to be a Dispatcher1Wrapper') } - process.stdout.write('mirrored') - } + const url = 'http://127.0.0.1:' + server.address().port + const res = await fetch(url) + const body = await res.text() + + process.stdout.write(JSON.stringify({ + body, + count, + mirroredV2: globalThis[dispatcherV2Symbol] === agent + })) + + server.close() + })().catch((err) => { + console.error(err?.cause?.stack || err?.stack || err) + process.exit(1) + }) ` const result = runNode(script) assert.strictEqual(result.status, 0, result.stderr) - const expected = Number(process.versions.node.split('.', 1)[0]) === 22 ? 'mirrored' : 'skipped' - assert.strictEqual(result.stdout, expected) + const payload = JSON.parse(result.stdout) + assert.strictEqual(payload.body, 'ok') + assert.strictEqual(payload.count, 1) + assert.strictEqual(payload.mirroredV2, true) }) test('Dispatcher1Wrapper bridges legacy handlers to a new Agent', () => {