-
-
Notifications
You must be signed in to change notification settings - Fork 729
Closed
Labels
bugSomething isn't workingSomething isn't working
Description
Bug Description
When using an Agent with the clientTtl option, under certain conditions, an uncatchable ClientDestroyedError will be thrown.
Reproducible By
const undici = require('undici');
const tp = require('node:timers/promises');
async function main() {
const clientTtl = 1000;
const agent = new undici.Agent({ clientTtl });
console.log('request 1')
const resp = await undici.request("https://example.com", {
dispatcher: agent
});
console.log(resp.statusCode);
// do not consume or destroy the response body here
await tp.setTimeout(clientTtl * 2);
console.log('request 2')
const resp1 = await undici.request("https://example.com", {
dispatcher: agent
});
console.log(resp1.statusCode);
}
process.on('unhandledRejection', (e) => {
console.log('unhandled rejection', e)
})
void main();Output:
request 1
200
request 2
unhandled rejection ClientDestroyedError: The client is destroyed
at Client.close (/tmp/test/node_modules/undici/lib/dispatcher/dispatcher-base.js:52:19)
at /tmp/test/node_modules/undici/lib/dispatcher/dispatcher-base.js:41:14
at new Promise (<anonymous>)
at Client.close (/tmp/test/node_modules/undici/lib/dispatcher/dispatcher-base.js:40:14)
at [close] (/tmp/test/node_modules/undici/lib/dispatcher/pool-base.js:124:41)
at Pool.close (/tmp/test/node_modules/undici/lib/dispatcher/dispatcher-base.js:79:17)
at /tmp/test/node_modules/undici/lib/dispatcher/dispatcher-base.js:41:14
at new Promise (<anonymous>)
at Pool.close (/tmp/test/node_modules/undici/lib/dispatcher/dispatcher-base.js:40:14)
at closeClientIfUnused (/tmp/test/node_modules/undici/lib/dispatcher/agent.js:95:31) {
code: 'UND_ERR_DESTROYED'
}
200
Expected Behavior
There should not be any error.
Environment
Node v24.13.0
Undici 7.20.0
Additional context
- In fix: memory leak in Agent #4425 I made the change to close the client (
Poolby default) when it's no longer used. However, theclientTtloption isn't considered, which means the pool will always be destroyed when it stops having an active connection. - In
Pool.[kGetDispatcher], the client is removed from the Pool if it has been alive for too long. InPoolBase.[kRemoveClient], the client is only removed from the pool's client list in the callback. IfPoolBase.[kClose]gets called after the client is destroyed but before the callback happens, the client will be closed again, triggering this issue.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working