From 306826ecb976cba878d54b95af4fcb02e1923439 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 27 Feb 2026 19:51:48 +0100 Subject: [PATCH] Add per-candidate retries (3x) for update downloads Each download URL is now retried up to 3 times with increasing delay (1.5s, 3s) before falling back to the next candidate URL. Recoverable errors (404, 403, 429, 5xx, timeout, network) trigger retries. Co-Authored-By: Claude Opus 4.6 --- src/main/update.ts | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/main/update.ts b/src/main/update.ts index c26a5f3..92388c8 100644 --- a/src/main/update.ts +++ b/src/main/update.ts @@ -12,6 +12,8 @@ import { logger } from "./logger"; const RELEASE_FETCH_TIMEOUT_MS = 12000; const CONNECT_TIMEOUT_MS = 30000; +const RETRIES_PER_CANDIDATE = 3; +const RETRY_DELAY_MS = 1500; const UPDATE_USER_AGENT = `RD-Node-Downloader/${APP_VERSION}`; type ReleaseAsset = { @@ -300,23 +302,46 @@ async function downloadFile(url: string, targetPath: string): Promise { logger.info(`Update-Download abgeschlossen: ${targetPath}`); } -async function downloadFromCandidates(candidates: string[], targetPath: string): Promise { - let lastError: unknown = new Error("Update Download fehlgeschlagen"); +async function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} - logger.info(`Update-Download: ${candidates.length} Kandidat(en)`); - for (let index = 0; index < candidates.length; index += 1) { - const candidate = candidates[index]; +async function downloadWithRetries(url: string, targetPath: string): Promise { + let lastError: unknown; + for (let attempt = 1; attempt <= RETRIES_PER_CANDIDATE; attempt += 1) { try { - await downloadFile(candidate, targetPath); + await downloadFile(url, targetPath); return; } catch (error) { lastError = error; - logger.warn(`Update-Download Kandidat ${index + 1}/${candidates.length} fehlgeschlagen: ${compactErrorText(error)}`); try { await fs.promises.rm(targetPath, { force: true }); } catch { // ignore } + if (attempt < RETRIES_PER_CANDIDATE && isRecoverableDownloadError(error)) { + logger.warn(`Update-Download Retry ${attempt}/${RETRIES_PER_CANDIDATE} für ${url}: ${compactErrorText(error)}`); + await sleep(RETRY_DELAY_MS * attempt); + continue; + } + break; + } + } + throw lastError; +} + +async function downloadFromCandidates(candidates: string[], targetPath: string): Promise { + let lastError: unknown = new Error("Update Download fehlgeschlagen"); + + logger.info(`Update-Download: ${candidates.length} Kandidat(en), je ${RETRIES_PER_CANDIDATE} Versuche`); + for (let index = 0; index < candidates.length; index += 1) { + const candidate = candidates[index]; + try { + await downloadWithRetries(candidate, targetPath); + return; + } catch (error) { + lastError = error; + logger.warn(`Update-Download Kandidat ${index + 1}/${candidates.length} endgültig fehlgeschlagen: ${compactErrorText(error)}`); if (index < candidates.length - 1 && isRecoverableDownloadError(error)) { continue; }