ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Axios 重试与错误恢复实战:基于响应拦截器实现 Retry、指数退避与 429 限流处理

2026/9/5 16:16:20 拓冰建站 浏览量
Axios 重试与错误恢复实战:基于响应拦截器实现 Retry、指数退避与 429 限流处理 Axios 重试与错误恢复实战基于响应拦截器实现 Retry、指数退避与 429 限流处理【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axiosAxios 本身没有内置的“自动重试”配置项但它的响应拦截器机制为重试策略提供了干净的挂载点。本文以 axios 仓库文档 Retry and error recovery 为主线完整实现并逐层拆解四种生产级重试模式基础重试、指数退避、429 Retry-After 限流重试、按请求关闭重试最后结合AbortController实现重试等待期的取消并对照仓库源码说明每一步的底层执行路径让你既能复制即用又清楚每次重试在 axios 内部到底走了哪条链。为什么把重试放在响应拦截器里网络请求可能因为瞬时原因失败服务器短暂抖动5xx、网络闪断无响应、限流429。如果在每个业务调用处手写重试循环代码会迅速变得臃肿且不一致。而 axios 的响应拦截器天然处在“适配器拒绝 → 业务代码 catch”之间的链路上适配器对非 2xx 状态通过settle构造AxiosError并 reject Promise这个 reject 会沿着 Promise 链传递到响应拦截器的rejected回调。因此拦截器是拦截错误、改写命运重试或放行的最佳位置业务代码无需感知任何重试细节。从源码结构看这条链路在 lib/core/Axios.js 中构建_request先把所有响应拦截器的fulfilled/rejected对收集进responseInterceptorChain再拼接进以dispatchRequest为起点的 Promise 链// lib/core/Axios.js节选构建 Promise 链的部分 const chain [dispatchRequest.bind(this), undefined]; chain.unshift(...requestInterceptorChain); chain.push(...responseInterceptorChain); promise Promise.resolve(config); while (i len) { promise promise.then(chain[i], chain[i]); }也就是说重试拦截器返回Promise.reject(error)时错误继续向后传最终落到你代码的catch而它返回api(config)时则是一次全新的request调用——整条链配置合并、请求拦截器、dispatchRequest、全部响应拦截器会重新走一遍。这就是“在拦截器里重试”能成立的机制基础。拦截器注册本身由 lib/core/InterceptorManager.js 的use(fulfilled, rejected, options)完成返回一个可用于eject(id)移除的数字 ID每个 Axios 实例在构造时即持有独立的request与response两个拦截器栈见 lib/core/Axios.js。基础重试用响应拦截器重发失败请求最简单的做法是捕获特定错误网络错误或 5xx把原始请求立即重发有限次。以下是文档给出的完整实现import axios from axios; const api axios.create({ baseURL: https://api.example.com }); const MAX_RETRIES 3; api.interceptors.response.use( (response) response, async (error) { const config error.config; // 仅对网络错误或 5xx 服务端错误重试 const shouldRetry !error.response || (error.response.status 500 error.response.status 600); if (!shouldRetry) { return Promise.reject(error); } config._retryCount config._retryCount ?? 0; if (config._retryCount MAX_RETRIES) { return Promise.reject(error); } config._retryCount 1; return api(config); } );这段代码里有几个关键细节每一个都能在源码中找到对应error.config为什么可用lib/core/settle.js 在validateStatus判定失败时把response.config作为第三个参数传入AxiosError构造函数网络错误无响应则由适配器把 config 附加到错误对象上。因此重试拦截器可以直接拿到完整配置包括 url、headers、data从而用api(config)原样重发。重试计数为什么要挂在 config 上config 对象是贯穿整次请求生命周期的载体且config._retryCount ?? 0的写法空值合并只在字段缺失时初始化之后每次重试都在同一个对象上累加。由于重试通过api(config)重新进入requestmergeConfig会把这份带计数的配置带下去计数因此跨重试存活直到 MAX_RETRIES时return Promise.reject(error)终止。shouldRetry的判定逻辑!error.response覆盖连接失败、超时等无响应场景status 500 status 600覆盖服务端错误。4xx尤其是 400/401/403/404通常意味着请求本身有问题立即拒绝、不做无谓重发。需要注意的边界!error.response对取消CanceledError同样为真因此如果你的流程中存在用户主动取消建议在shouldRetry前面加一道if (axios.isCancel(error)) return Promise.reject(error);避免把被取消的请求当成“网络错误”重发——isCancel的实现只是检查错误对象上的__CANCEL__标记见 lib/cancel/isCancel.js。指数退避给受压的服务器留出喘息时间失败后立即重试对一个已经吃力的服务器是额外负担。指数退避exponential backoff让每次重试的等待时间成倍增长const delay (ms) new Promise((resolve) setTimeout(resolve, ms)); api.interceptors.response.use( (response) response, async (error) { const config error.config; const shouldRetry !error.response || (error.response.status 500 error.response.status 600); if (!shouldRetry) return Promise.reject(error); config._retryCount config._retryCount ?? 0; if (config._retryCount 3) return Promise.reject(error); config._retryCount 1; // 每次重试前等待 200ms、400ms、800ms…… const backoff 100 * 2 ** config._retryCount; await delay(backoff); return api(config); } );注意config._retryCount在计算 backoff之前先自增第 1 次重试等待100 * 2 ** 1 200ms第 2 次400ms第 3 次800ms与注释一致。这个“先计数、后计算”的顺序是该公式成立的前提照抄时不要调整自增位置。从行为上看await delay(backoff)使拦截器的rejected异步回调在 setTimeout 之后才返回api(config)整个重试等待发生在拦截器内部、对业务调用方完全透明——业务代码只感知到一个延迟更久的最终 resolve/reject。如果想给退避加上抖动jitter避免多个客户端同步重试可以直接在backoff上乘一个随机系数思路不变。429 限流重试尊重 Retry-After 头当服务器返回429 Too Many Requests时通常会附带Retry-After头精确告知应等待的秒数。此时重试策略应当“听服务器的”api.interceptors.response.use( (response) response, async (error) { const config error.config; if (error.response?.status ! 429) return Promise.reject(error); config._retryCount config._retryCount ?? 0; if (config._retryCount 3) return Promise.reject(error); config._retryCount 1; const retryAfterHeader error.response.headers[retry-after]; const waitMs retryAfterHeader ? parseFloat(retryAfterHeader) * 1000 // 该头以秒为单位 : 1000; // 默认等待 1 秒 await new Promise((resolve) setTimeout(resolve, waitMs)); return api(config); } );两个实现细节值得展开小写的retry-after为什么能取到值axios 在适配层和dispatchRequest中都会把响应头归一化为AxiosHeaders实例lib/core/AxiosHeaders.js 内部对头名执行name.toLowerCase()归一化因此按小写键读取retry-after是可靠写法无需关心服务器实际发送的是Retry-After还是retry-after。缺省兜底服务器有时只返回 429 而不给Retry-After此时parseFloat分支不成立回落到 1 秒的保守默认值配合最多 3 次重试封顶避免无限挂起。这个 429 处理与 axios 文档中的 Rate limitingNode.js HTTP 适配器侧的maxRate带宽限制是两个方向maxRate控制“我发多快”而 429 重试处理的是“对方让我慢下来”两者可以在同一个实例上同时使用。按请求关闭重试保护非幂等写操作重试对幂等请求典型如 GET通常安全但对不幂等的变更操作如扣款、下单重复执行可能造成业务事故。文档给出的做法是在配置上加一个标记让指定请求豁免重试// 在重试拦截器的重试逻辑之前加入这一行 if (config._noRetry) return Promise.reject(error); // 对特定调用关闭重试 await api.post(/payments/charge, body, { _noRetry: true });之所以自定义字段可以直接塞进 per-request 配置是因为request入口处的mergeConfig(this.defaults, config)对未知键不做校验性剔除_noRetry这类下划线前缀字段会随 config 一路带到拦截器。这条约定下划线前缀 配置字段携带重试状态与_retryCount是同一套模式借助 config 对象的持久性在拦截器闭包外也能实现“每请求私有状态”。重试与取消结合AbortController 中止等待中的请求指数退避意味着请求可能在两次尝试之间沉睡数秒。此时若用户已经离开页面或不再需要结果应当能主动中断。文档的方案是AbortControllersignalconst controller new AbortController(); try { await api.get(/api/data, { signal: controller.signal }); } catch (error) { if (axios.isCancel(error)) { console.log(Request aborted by user); } } // 从别处取消请求以及一切进行中的重试等待 controller.abort();axios 对signal的处理入口在 lib/core/dispatchRequest.jsfunction throwIfCancellationRequested(config) { if (config.cancelToken) { config.cancelToken.throwIfRequested(); } if (config.signal config.signal.aborted) { throw new CanceledError(null, config); } }throwIfCancellationRequested会在请求发出前、适配器 resolve 后、以及适配器 reject 时被检查同一文件第 41、56、74 行。一旦signal.aborted为真抛出携带__CANCEL__ true标记的 CanceledError业务代码即可用axios.isCancel(error)与真正的网络错误区分开。对重试拦截器来说有一个实践要点CanceledError同样表现为“无response的 reject”若不显式排除基础重试的!error.response分支会把它误判为可重试的网络错误。因此完整的重试拦截器建议在判定链最前面加上axios.isCancel短路这与文档 Cancellation 中取消语义保持一致。需要说明的是拦截器内setTimeout等待期间本身不受signal自动打断若需要“等待期间取消也能立即退出”可以在delay辅助函数中监听signal的abort事件提前 resolve属于文档未覆盖的增强方向。小结与延伸阅读重试策略完全托管在响应拦截器的rejected回调中判定网络错误/5xx/429→ 计数config._retryCount→ 退避固定/指数/Retry-After→api(config)重发整条请求链。每次重发都经过完整的request管线配置合并、请求拦截器、dispatchRequest因此请求拦截器里的鉴权、签名逻辑在每次重试上都会生效。用config._noRetry为非幂等写操作豁免重试用AbortController的signal中止重试流并用axios.isCancel在拦截器中排除取消错误。相关文档可继续深入Interceptors、Error handling、Cancellation、Promises关键源码位置lib/core/Axios.js拦截器 Promise 链、lib/core/settle.js错误构造与error.config来源、lib/core/InterceptorManager.jsuse注册、lib/cancel/CanceledError.js 与 lib/cancel/isCancel.js取消判定。【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考