Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/api/Dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

Expand Down
18 changes: 7 additions & 11 deletions lib/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand All @@ -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 () {
Expand Down
52 changes: 40 additions & 12 deletions test/node-test/global-dispatcher-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading