You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When using an Agent with the clientTtl option, under certain conditions, an uncatchable ClientDestroyedError will be thrown.
Reproducible By
constundici=require('undici');consttp=require('node:timers/promises');asyncfunctionmain(){constclientTtl=1000;constagent=newundici.Agent({ clientTtl });console.log('request 1')constresp=awaitundici.request("https://example.com",{dispatcher: agent});console.log(resp.statusCode);// do not consume or destroy the response body hereawaittp.setTimeout(clientTtl*2);console.log('request 2')constresp1=awaitundici.request("https://example.com",{dispatcher: agent});console.log(resp1.statusCode);}process.on('unhandledRejection',(e)=>{console.log('unhandled rejection',e)})voidmain();
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 (Pool by default) when it's no longer used. However, the clientTtl option 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. In PoolBase.[kRemoveClient], the client is only removed from the pool's client list in the callback. If PoolBase.[kClose] gets called after the client is destroyed but before the callback happens, the client will be closed again, triggering this issue.
Bug Description
When using an
Agentwith theclientTtloption, under certain conditions, an uncatchableClientDestroyedErrorwill be thrown.Reproducible By
Output:
Expected Behavior
There should not be any error.
Environment
Node v24.13.0
Undici 7.20.0
Additional context
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.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.