Files
spark-store/src/composables/useHttp.ts
T
xiyidaiwa e1edf1dcd5 fix: fetchWithRetry 对 429 限流增加指数退避与 Retry-After 尊重
- 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 包装会破坏该语义,不改
2026-08-13 21:02:12 +08:00

101 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* useHttp —— 全局 HTTP 工具(axios 实例 + 带重试请求 + 超时包装)。
*
* 从原 App.vue 原样搬移,逻辑零改动,仅改为导出供各 composable 复用。
* 含缓存穿透拦截器(createCacheBusterInterceptor),与之前的审计加固一致。
*/
import axios, { AxiosError } from "axios";
import { APM_STORE_BASE_URL } from "../global/storeConfig";
import { createCacheBusterInterceptor } from "../global/cacheBusterInterceptor";
// Axios 全局配置
export const axiosInstance = axios.create({
baseURL: APM_STORE_BASE_URL,
timeout: 5000, // 增加到 5 秒,避免网络波动导致的超时
});
// C2:数据 JSONapplist / categories / sidebar-config 等)追加 ?_t 版本戳,
// 穿透 CDN 边缘缓存,确保返回最新列表。复用共享缓存穿透拦截器(带 TTL 复用戳,
// 避免同会话频繁击穿缓存)。与主进程 onBeforeSendHeaders 注入的 no-cache 互为兜底。
// 这是有意的缓存击穿策略(非缺陷),用于解决商店目录强缓存导致的更新延迟。
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 <T>(
path: string,
signal?: AbortSignal,
retries = 2,
retryDelayMs = 500,
): Promise<T | null> => {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const resp = await axiosInstance.get<T>(path, { signal });
return resp.data;
} catch (err) {
const ae = err as AxiosError;
const status = ae.response?.status;
// 4xx(含 404)快速失败,不重试
if (status && status < 500 && status !== 429) {
return null;
}
// 仅对网络错误 / 5xx / 429 重试
const retryable =
!status || RETRYABLE_STATUS.has(status) || status === 429;
if (!retryable || attempt === retries) {
return null;
}
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;
};
// 全局请求取消控制器(目录加载期间可整体取消)
export const rootAbortController = new AbortController();
// 给 Promise 包一层超时,超时即 reject。finally 中清理定时器,避免泄漏。
export const withTimeout = async <T>(
promise: Promise<T>,
ms: number,
label = "operation",
): Promise<T> => {
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Error(`${label} 超时 (${ms}ms)`)), ms);
});
try {
return await Promise.race([promise, timeout]);
} finally {
if (timer) clearTimeout(timer);
}
};