-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
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 not
Output 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.