Repository navigation
AbortSignal.any() is unreliable and breaks timeouts #57736
Description
Version
v23.11.0
Platform
Darwin Seth-Laptop.local 24.3.0 Darwin Kernel Version 24.3.0: Thu Jan 2 20:24:16 PST 2025; root:xnu-11215.81.4~3/RELEASE_ARM64_T6000 arm64
Subsystem
No response
What steps will reproduce the bug?
The code below reliably reproduces the problem for me. It should throw a TimeoutError but instead shows the log message.
Simple example with external server:
const signal = AbortSignal.any([AbortSignal.timeout(9000)]);
await fetch('https://httpstat.us/200?sleep=10000', { signal });
console.log('❌ should have thrown timeout error but did not');Multiple iterations with internal server to demonstrate flakiness:
import http from 'node:http';
const iterations = 10;
const timeout = 1000;
const responseDelay = 10_000;
const port = 3000;
// const url = `https://httpstat.us/200?sleep=${responseDelay}`;
const url = `http://localhost:${port}`;
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
setTimeout(() => {
res.end('Hello World!');
}, responseDelay);
});
await new Promise((resolve) => {
server.listen(port, resolve);
});
const test = async () => {
const results = [];
for (let i = 0; i < iterations; ++i) {
const startTime = Date.now();
let result;
try {
const signal = AbortSignal.any([AbortSignal.timeout(timeout)]);
await fetch(url, { signal });
result = '❌ no timeout';
}
catch (error) {
result = error.name === 'TimeoutError' ? '✔ ' : '❗ ';
result += error.name;
}
const endTime = Date.now();
const duration = endTime - startTime;
results.push({ result, startTime, endTime, duration });
}
return results;
};
console.table(await test());
server.close();How often does it reproduce? Is there a required condition?
Reproduces 100% of the time for me with the specific examples above, but does not occur on every iteration in the multiple iterations example, in fact it seems to only ever occur once (at least for small iteration counts, I haven't tried more than 50). Interestingly, which timeout fails seems to change depending on the value of timeout (the timeout length), but is stable across runs given the same timeout. Moving code around within the file also seems to affect it sometimes.
What is the expected behavior? Why is that the expected behavior?
All timeouts should fire and cause fetch() to throw a TimeoutError.
What do you see instead?
At least one timeout does not fire and fetch() returns a response instead of throwing a TimeoutError.
Output for simple example:
❯ node fetch.js
❌ should have thrown timeout error but did notOutput for multiple iteration example:
❯ node fetch.js
┌─────────┬──────────────────┬───────────────┬───────────────┬──────────┐
│ (index) │ result │ startTime │ endTime │ duration │
├─────────┼──────────────────┼───────────────┼───────────────┼──────────┤
│ 0 │ '✔ TimeoutError' │ 1743645479368 │ 1743645480377 │ 1009 │
│ 1 │ '✔ TimeoutError' │ 1743645480377 │ 1743645481380 │ 1003 │
│ 2 │ '✔ TimeoutError' │ 1743645481380 │ 1743645482385 │ 1005 │
│ 3 │ '✔ TimeoutError' │ 1743645482385 │ 1743645483388 │ 1003 │
│ 4 │ '✔ TimeoutError' │ 1743645483388 │ 1743645484390 │ 1002 │
│ 5 │ '✔ TimeoutError' │ 1743645484390 │ 1743645485393 │ 1003 │
│ 6 │ '✔ TimeoutError' │ 1743645485393 │ 1743645486395 │ 1002 │
│ 7 │ '✔ TimeoutError' │ 1743645486396 │ 1743645487399 │ 1003 │
│ 8 │ '❌ no timeout' │ 1743645487399 │ 1743645497417 │ 10018 │
│ 9 │ '✔ TimeoutError' │ 1743645497417 │ 1743645498421 │ 1004 │
└─────────┴──────────────────┴───────────────┴───────────────┴──────────┘Additional information
Notice that on the iteration where the timeout fails to fire, the duration of the iteration takes the full responseDelay time. This is because the request was not aborted, even though it should have been.
In my effort to make a minimum reproducible example, I found that doing so requires AbortSignal.any(). In the real world, I am passing multiple signals to it. But that doesn't seem to affect these examples. See also #54614 and #57584 which indicate that AbortSignal.any() has memory leak problems. However, it seems to be reproducible in much simpler scenarios.
Activity
gurgunday commentedon Apr 13, 2025
I can confirm the issue.
chore: restore @rcrsr/rill engines.node to >=22.16.0
chore: restore @rcrsr/rill engines.node to >=22.16.0
chore: bump toolchain and dev dependencies (#63)
fix(cloud): roll the request deadline by hand instead of AbortSignal.any
docs: design for timeouts and deadlines
feat: bound the three unguarded provider paths (timeouts S1+S2)
fix(retry): bound a failing call and stop retries moving in lockstep
fix(leafly): bound the Supabase auth transport so acknowledge cannot …
build: require Node 22.16 or later
0.26.2 — deadline(): no AbortSignal.timeout inside AbortSignal.any (#20)