From e4fb2fd1b059ede15652046a248d72ccda0725d6 Mon Sep 17 00:00:00 2001 From: UNITRONIX <36471318+UNITRONIX@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:22:18 +0200 Subject: [PATCH] feat(update): implement automatic retry for GitHub raw file downloads - Added functionality to retry downloads from GitHub on encountering rate limit and server error status codes (429, 502, 503, 504) with exponential backoff. - Updated the changelog to reflect the new panel updater feature that enhances locale sync reliability during large updates. - Included tests for the new retry logic to ensure proper handling of retryable status codes. --- CHANGELOG.md | 1 + web-nodejs/services/updateService.js | 38 ++++++++++++++++++- .../tests/updateService.consoleSync.test.js | 11 ++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfdf2053..90b36617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Added - **Web Remote — File Transfer drop zone:** left panel shows a drag-and-drop upload box (click to pick files, multi-file supported). Uploads go to the folder currently open on the remote side; progress appears in the Transfers column. - **Web Remote — RustDesk file transfer parity:** 128 KB blocks, zstd upload compression, resume via `offset_blk` / `transferred_size`, overwrite confirmation dialog (Skip / Overwrite / apply to all), remote context menu (rename, delete, new folder), hidden-files toggle, and Resume in the transfer queue after errors. +- **Panel updater:** GitHub raw file downloads retry automatically on HTTP 429/502/503/504 (exponential backoff), reducing failed locale syncs during large updates. ### Changed - _(none yet)_ diff --git a/web-nodejs/services/updateService.js b/web-nodejs/services/updateService.js index 7855a16c..86f8c835 100644 --- a/web-nodejs/services/updateService.js +++ b/web-nodejs/services/updateService.js @@ -349,9 +349,17 @@ function ghGet(urlPath, { bypassCache = false } = {}) { } /** - * Download raw file content from GitHub (binary-safe). + * Download raw file content from GitHub (binary-safe), with retry on rate limits. */ -function ghDownloadFile(owner, repo, ref, filePath) { +function isRetryableDownloadStatus(statusCode) { + return statusCode === 429 || statusCode === 502 || statusCode === 503 || statusCode === 504; +} + +function getDownloadRetryDelayMs(attempt) { + return Math.min(1000 * Math.pow(2, Math.max(0, attempt - 1)), 15000); +} + +function ghDownloadFileOnce(owner, repo, ref, filePath) { const url = `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(ref)}/${filePath}`; return new Promise((resolve, reject) => { const headers = { 'User-Agent': USER_AGENT }; @@ -376,6 +384,30 @@ function ghDownloadFile(owner, repo, ref, filePath) { }); } +async function ghDownloadFile(owner, repo, ref, filePath, opts = {}) { + const maxAttempts = Number(opts.maxAttempts) > 0 ? Number(opts.maxAttempts) : 4; + let lastErr; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await ghDownloadFileOnce(owner, repo, ref, filePath); + } catch (err) { + lastErr = err; + const match = /Download failed \((\d+)\)/.exec(err.message || ''); + const statusCode = match ? Number(match[1]) : 0; + if (!isRetryableDownloadStatus(statusCode) || attempt >= maxAttempts) { + throw err; + } + const delayMs = getDownloadRetryDelayMs(attempt); + console.warn( + `[UPDATE] Download retry ${attempt}/${maxAttempts} for ${filePath}` + + ` after ${delayMs}ms (${err.message})` + ); + await new Promise((r) => setTimeout(r, delayMs)); + } + } + throw lastErr; +} + // ======================== Docker image deployment ======================== function shasMatch(a, b) { @@ -3256,6 +3288,8 @@ module.exports = { isNonCriticalUpdateFailure, GITHUB_COMPARE_FILE_LIMIT, isCompareLikelyTruncated, + isRetryableDownloadStatus, + getDownloadRetryDelayMs, resolveConsoleRequire, collectConsoleRequiredFiles, isResolvedByIndexModule, diff --git a/web-nodejs/tests/updateService.consoleSync.test.js b/web-nodejs/tests/updateService.consoleSync.test.js index aeae7f5d..6185d0f5 100644 --- a/web-nodejs/tests/updateService.consoleSync.test.js +++ b/web-nodejs/tests/updateService.consoleSync.test.js @@ -4,6 +4,8 @@ const { createConsoleDeployGraph } = require('../lib/consoleDeployGraph'); const { GITHUB_COMPARE_FILE_LIMIT, isCompareLikelyTruncated, + isRetryableDownloadStatus, + getDownloadRetryDelayMs, } = require('../services/updateService'); describe('updateService console sync helpers', () => { @@ -50,4 +52,13 @@ describe('updateService console sync helpers', () => { expect(required.has('routes/auth.routes.js')).toBe(true); expect(required.has('services/serverAttestation.js')).toBe(true); }); + + test('retries GitHub raw downloads on rate limit status codes', () => { + expect(isRetryableDownloadStatus(429)).toBe(true); + expect(isRetryableDownloadStatus(503)).toBe(true); + expect(isRetryableDownloadStatus(404)).toBe(false); + expect(getDownloadRetryDelayMs(1)).toBe(1000); + expect(getDownloadRetryDelayMs(2)).toBe(2000); + expect(getDownloadRetryDelayMs(6)).toBe(15000); + }); });