diff --git a/lib/dispatcher/agent.js b/lib/dispatcher/agent.js index 858a5f248f7..1e670746e8e 100644 --- a/lib/dispatcher/agent.js +++ b/lib/dispatcher/agent.js @@ -107,15 +107,15 @@ class Agent extends DispatcherBase { } let hasOrigin = false - for (const client of this[kClients].values()) { - if (client[kUrl].origin === dispatcher[kUrl].origin) { + for (const k of this[kClients].keys()) { + if (k === origin || k === `${origin}#http1-only`) { hasOrigin = true break } } if (!hasOrigin) { - this[kOrigins].delete(dispatcher[kUrl].origin) + this[kOrigins].delete(origin) } } diff --git a/test/agent-connection-management.js b/test/agent-connection-management.js index cd520a304de..61312e11bf3 100644 --- a/test/agent-connection-management.js +++ b/test/agent-connection-management.js @@ -1,7 +1,9 @@ const { test, describe } = require('node:test') const assert = require('node:assert') const { createServer } = require('node:http') -const { request, Agent, Pool } = require('..') +const { once } = require('node:events') +const { request, Agent, Pool, ProxyAgent } = require('..') +const { kClients } = require('../lib/core/symbols') // https://github.com/nodejs/undici/issues/4424 describe('Agent should close inactive clients', () => { @@ -154,3 +156,47 @@ describe('Agent should not close active clients', () => { assert.deepEqual(socketSequence.slice(3), ['2', '2']) }) }) + +// https://github.com/nodejs/undici/issues/5529 +describe('Agent teardown of factory dispatchers without an internal url', () => { + test('ProxyAgent forwarding plain http does not crash on teardown', async (t) => { + // A minimal forward proxy: answers any absolute-form request itself. + const proxy = createServer((req, res) => { + res.setHeader('connection', 'close') + res.end('ok') + }).listen(0) + + t.after(() => { + proxy.closeAllConnections?.() + proxy.close() + }) + await once(proxy, 'listening') + + const proxyAgent = new ProxyAgent(`http://localhost:${proxy.address().port}`) + t.after(() => proxyAgent.close()) + + // A plain-http request registers an Http1ProxyWrapper, which has no kUrl, + // in the inner Agent's client map. + const { statusCode, body } = await request('http://target.example/', { dispatcher: proxyAgent }) + assert.equal(statusCode, 200) + await body.text() + + let innerAgent + for (const sym of Object.getOwnPropertySymbols(proxyAgent)) { + const value = proxyAgent[sym] + if (value && value[kClients] instanceof Map && value[kClients].size > 0) { + innerAgent = value + break + } + } + assert.ok(innerAgent, 'expected to find the inner Agent') + const [wrapper] = innerAgent[kClients].values() + + // The Agent subscribes to this event on every dispatcher its factory + // returns. Before the fix this threw + // "Cannot read properties of undefined (reading 'origin')". + wrapper.emit('disconnect', 'http://target.example', [wrapper], new Error('closed')) + + assert.equal(innerAgent[kClients].size, 0, 'expected the unused wrapper to be removed') + }) +})