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
{{ message }}
Repository navigation
net: socket.setTimeout() from a nextTick fires the old timeout at once and drops the new one (regression from #64254) #67047
v24.19.0, v24.22.1, v26.12.1. Last good: v24.18.1.
Platform
Linux 6.10.14-linuxkit aarch64 (official Docker images). We first saw it on Linux in production.
Subsystem
timers, net
What steps will reproduce the bug?
'use strict';constnet=require('net');constserver=net.createServer(()=>{}).listen(0,()=>{constsocket=net.connect(server.address().port,()=>{letrearmedAt=0;socket.on('timeout',()=>{console.log(`${process.version}: 'timeout' ${Date.now()-rearmedAt} ms after re-arm (socket.timeout=${socket.timeout})`);});setTimeout(()=>process.nextTick(()=>{socket.setTimeout(1500);rearmedAt=Date.now();}),300);socket.setTimeout(300);setTimeout(()=>{socket.destroy();server.close();},2500);});});
How often does it reproduce? Is there a required condition?
Every time. The socket's timeout has to be due in the same timer pass as the timer just before it in the same list, and that timer re-arms the socket from a nextTick or microtask. A synchronous re-arm, or one from setImmediate, works.
What is the expected behavior? Why is that the expected behavior?
v24.18.1: 'timeout' 1509 ms after re-arm (socket.timeout=1500)
The re-arm replaces the 300 ms timeout, so only the new one should fire.
What do you see instead?
v24.22.1: 'timeout' 1 ms after re-arm (socket.timeout=1500)
v26.12.1: 'timeout' 0 ms after re-arm (socket.timeout=1500)
The old timeout fires at once, and the new one never fires.
Additional information
#64254 (in 24.19.0 and 26.5.0) made setStreamTimeout reuse the socket's Timeout object. listOnTimeout checks that the timer is due, runs the next tick queue, then fires it (timers.js#L623-L642). The re-arm in that tick reuses the same object (timers.js#L458-L480, called from stream_base_commons.js#L258-L267), so it still runs. Before, the cleared object was skipped.
Re-checking the timer after the ticks run fixes the repro for us:
We hit this through HTTP keep-alive: a request started from a 5 s timer, on a socket idle for exactly the global agent's 5 s timeout, with req.setTimeout(), timed out at once. Passing the timeout option instead of calling req.setTimeout() avoids it.
This was investigated with a coding agent, which ran the repro on each version listed, in the official Docker images, several times each.
Activity