@actions/cache: downloadCacheHttpClientConcurrent never stops its progress timer on failure, so the step hangs until the job times out
I maintainer di solito rispondono entro 1 giorno
@YodHeVauHe ci sta già lavorando.
Dal 4/10/2026.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 82/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- typescript
- Ambito
- tooling
Direzione di ricerca
Inizia da packages/cache/src/internal/downloadUtils.ts, leggendo downloadCacheHttpClientConcurrent intorno al timer di avanzamento e al relativo blocco finally; confronta la relativa pulizia con downloadCacheStorageSDK. Esegui i comandi npm e repro.mjs forniti, quindi conferma che un download rifiutato interrompa l'output dell'avanzamento e consenta al processo di terminare.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Describe the bug
When downloadCacheHttpClientConcurrent throws, it never stops its DownloadProgress display timer. The timer reschedules itself every second until isDone(), which a failed download never reaches. So Node's event loop never empties, the calling action finishes its work, and the step prints Received 0 of N (0.0%), 0.0 MBs/sec until the job times out or someone cancels it.
The timer starts here:
This is the only cleanup, and it doesn't touch progress:
The timer re-arms itself on every tick until the download completes:
downloadCacheStorageSDK already stops its timer in finally:
This is related to #2470 but has a different cause. #2470 is the promiseWithTimeout timer, which holds the process for at most 10 minutes. This timer has no upper bound, and #2478 doesn't change it.
Observed in CI
actions/setup-node@v7 (bundles @actions/cache 6.1.0) and tailscale/github-action v4.2.0 (bundles 5.0.2), on third-party runners (Ubicloud). Each 4 MiB ranged GET got a 161-byte body back, so the first archiveDescriptor.write threw. The action logged the warning and carried on, but the step never exited.
setup-node: the step logged the failure and moved on, then kept printing progress for 15 minutes until the job timeout:
10:58:59.536 /home/runner/.pnpm-store/v11
10:59:00.391 ##[warning]Failed to restore: The value of "length" is out of range. It must be <= 161. Received 4194304
10:59:00.404 pnpm cache is not found
10:59:00.793 Received 58720256 of 436400958 (13.5%), 55.9 MBs/sec
... (the same byte count, once a second)
11:13:46.847 Received 58720256 of 436400958 (13.5%), 0.1 MBs/sec
11:13:46.919 ##[error]The operation was canceled.
tailscale: the action fell back to a direct download, joined the tailnet and finished at 12:24:50. The step then printed progress until the 5-minute step timeout:
12:24:45.188 ##[warning]Failed to restore: The value of "length" is out of range. It must be <= 161. Received 4194304
12:24:46.005 Received 0 of 34760007 (0.0%), 0.0 MBs/sec
12:24:50.613 Ping host ... reachable via direct connection!
... (Received 0 of 34760007, once a second)
12:29:44.556 ##[error]The action 'Join tailnet' has timed out after 5 minutes.
Another job without a step timeout stayed in Setup Node.js for over an hour before it was cancelled.
The trigger here was a misbehaving cache backend, which I'm raising with the provider separately. The hang doesn't depend on the trigger: any rejection inside the try block, including a failed write, a failed segment or a HEAD without Content-Length after the timer started, leaves the timer running.
To Reproduce
This uses the published 6.3.0 package against a local server that answers every ranged GET with a short 200:
npm i @actions/[email protected]
timeout 300 node repro.mjs; echo "exit=$?"
// repro.mjs
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import { downloadCacheHttpClientConcurrent } from './node_modules/@actions/cache/lib/internal/downloadUtils.js';
const LENGTH = 34760007;
const server = http.createServer((req, res) => {
if (req.method === 'HEAD') {
res.writeHead(200, { 'Content-Length': LENGTH });
return res.end();
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'x'.repeat(161 - 12) }));
});
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const url = `http://127.0.0.1:${server.address().port}/cache.tzst`;
try {
await downloadCacheHttpClientConcurrent(url, path.join(os.tmpdir(), 'repro.tzst'), {
timeoutInMs: 30000,
downloadConcurrency: 8,
});
} catch (error) {
console.log(`rejected: ${error.message}`);
}
server.close();
console.log('download settled, server closed');
Output on Node 24.18.0:
rejected: The value of "length" is out of range. It must be <= 161. Received 4194304
download settled, server closed
Received 0 of 34760007 (0.0%), 0.0 MBs/sec
Received 0 of 34760007 (0.0%), 0.0 MBs/sec
...
exit=124
In this run it printed 299 progress lines after the rejection before timeout killed it at 300 s.
Expected behavior
Once the download settles, whether it succeeded or failed, the display timer stops and the process can exit.
Suggested fix
Hoist progress out of the try and stop it in finally, as downloadCacheStorageSDK already does:
const archiveDescriptor = await fs.promises.open(archivePath, 'w')
const httpClient = new HttpClient('actions/cache', undefined, {
socketTimeout: options.timeoutInMs,
keepAlive: true
})
+ let progress: DownloadProgress | undefined
try {
...
- const progress = new DownloadProgress(length)
+ progress = new DownloadProgress(length)
progress.startDisplayTimer()
...
} finally {
+ progress?.stopDisplayTimer()
httpClient.dispose()
await archiveDescriptor.close()
}
I applied the equivalent change to the compiled 6.3.0 output and ran the repro against it:
| Build | Process exits after the download rejects |
|---|---|
| 6.3.0 as published | never (killed at 300 s) |
| + the change above | 80 s |
| + the change above and #2478 | 61 s |
What keeps the process alive for the remaining ~60 s: segment downloads that were already in flight when the write threw keep retrying (downloadSegmentRetry, 5 retries, each with a 5 s retryHttpClientResponse backoff). Each attempt fails with Client has already been disposed. Nothing cancels them when the download is abandoned. That's secondary, bounded, and probably worth its own change. The display timer is the part that never ends.
Additional context
@actions/cache6.3.0 is affected, and so ismainat85d7331. So are the copies bundled inactions/setup-node@v7(6.1.0) andtailscale/github-actionv4.2.0 (5.0.2).- Linux, Node 24.
- This path is used only when the archive hostname ends in
.blob.core.windows.net(see #2487).
- Lingua principale
- TypeScript
- Stelle
- 5.9k
- Fork
- 1.8k
- Merge medio
- 3g 11h
- PR unite (30g)
- 7
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di actions/toolkit
-
enhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno
-
@actions/cache: a failed cache download leaks its segment timer and stalls the job for up to 10 minutesForse già presa @Boulea7 l’ha presa 82 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
actions/toolkit#2470 · 2 commenti · 2 reazioni ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
actions/toolkit#2408 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
properly join URLsForse già presa @jjoseph456 l’ha presa 168 giorni fa. Apertabug
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 1-3 ore Idoneità per principianti 72/100
actions/toolkit#2361 · 4 commenti · 12 reazioni ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di actions/toolkit
Issue simili
-
needs-area
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
Azure/typespec-azure#5936 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 Mezza giornata Idoneità per principianti 72/100
vercel-labs/scriptc#2910 ·
I maintainer di solito rispondono entro 1 giorno
-
automation missing-model model-sync provider:fireworks-ai
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
anomalyco/models.dev#12458 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
I maintainer di solito rispondono entro 1 giorno