From e1edf1dcd50c3592c7b18bfde24bb91ef35fb3c0 Mon Sep 17 00:00:00 2001 From: xiyidaiwa Date: Thu, 13 Aug 2026 21:02:12 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20fetchWithRetry=20=E5=AF=B9=20429=20?= =?UTF-8?q?=E9=99=90=E6=B5=81=E5=A2=9E=E5=8A=A0=E6=8C=87=E6=95=B0=E9=80=80?= =?UTF-8?q?=E9=81=BF=E4=B8=8E=20Retry-After=20=E5=B0=8A=E9=87=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useHttp.calculateRetryDelay:优先读服务端 Retry-After 头;429 用指数退避 (1000*2^attempt, 封顶 30s);其余 5xx 仍固定延迟,避免限流场景频繁重试浪费带宽 - debian/changelog 测试版本号 5.2.1.2-test - 验证:vue-tsc 0 / eslint 0 / dpkg-buildpackage 打包成功 审查项实证(其余 3 项均误报/已有防护,不改): - ① install-manager pkgname 二次校验:已有 PKGNAME_PATTERN(/^[a-zA-Z0-9._+-]+$/) 在 parseAppPayload/addInstallTask/launch-app 三入口校验,且 spawn 全用 shell:false 参数逐元素传递,命令注入不可达;审查建议的 replace 属冗余纵深防御,不改 - ② InstalledAppsModal /var/lib/ 宽泛:误报,真实白名单(400-409行)已是 /var/lib/apm/ 与 /var/lib/dpkg/info/ 具体子目录,无宽泛 /var/lib/ 项 - ③ useAppState.ts 注入模式缺注册检查:误报,useAppState.ts 无 registerRequireLogin; 真实 registerRequireLogin 在 useFavorites/useAppDetail,默认 ()=>true 为刻意安全兜底 (未注册=放行),审查建议的 InjectionPoint 包装会破坏该语义,不改 --- debian/changelog | 2 +- src/composables/useHttp.ts | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 02076836..c945eae2 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -spark-store (5.2.1.1-test) UNRELEASED; urgency=medium +spark-store (5.2.1.2-test) UNRELEASED; urgency=medium * Initial release. (Closes: #nnnn) diff --git a/src/composables/useHttp.ts b/src/composables/useHttp.ts index ba11d82e..d06a63f9 100644 --- a/src/composables/useHttp.ts +++ b/src/composables/useHttp.ts @@ -23,6 +23,24 @@ axiosInstance.interceptors.request.use(createCacheBusterInterceptor()); // 5xx / 网络错误 / 超时重试;4xx(如 404)快速失败 const RETRYABLE_STATUS = new Set([502, 503, 504]); +// 计算重试延迟:优先尊重服务端 Retry-After 头;429 限流用指数退避(封顶 30s); +// 其余 5xx 用固定延迟。避免对限流场景频繁重试浪费带宽。 +const calculateRetryDelay = ( + status: number | undefined, + attempt: number, + retryAfterHeader?: string, + fixedDelayMs = 500, +): number => { + if (retryAfterHeader) { + const seconds = Number.parseInt(retryAfterHeader, 10); + if (!Number.isNaN(seconds) && seconds > 0) return seconds * 1000; + } + if (status === 429) { + return Math.min(1000 * 2 ** attempt, 30000); + } + return fixedDelayMs; +}; + export const fetchWithRetry = async ( path: string, signal?: AbortSignal, @@ -46,7 +64,16 @@ export const fetchWithRetry = async ( if (!retryable || attempt === retries) { return null; } - await new Promise((r) => setTimeout(r, retryDelayMs)); + const retryAfter = ae.response?.headers?.["retry-after"] as + | string + | undefined; + const delay = calculateRetryDelay( + status, + attempt, + retryAfter, + retryDelayMs, + ); + await new Promise((r) => setTimeout(r, delay)); } } return null;