
axios 重试与错误恢复实战用响应拦截器实现重试、指数退避与 429 限流恢复【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios网络请求经常因为瞬时原因失败——服务器短暂抖动、网络闪断、限流响应等。axios 本身没有在配置项层面提供内建重试次数这样的开箱即用开关官方推荐的落地方案是在**响应拦截器response interceptor**中实现重试策略见 docs/pages/advanced/retry.md。读完本篇你将掌握如何用拦截器对网络错误和 5xx 进行有限次重试、如何实现指数退避避免压垮故障服务器、如何解析Retry-After头处理 429 限流、如何按单个请求粒度关闭重试以及如何与AbortController取消机制协同工作——所有示例均可直接复制运行。一、为什么用拦截器实现重试重试的核心诉求是透明处理瞬时故障不污染业务代码。响应拦截器天然处于HTTP 状态码非 2xx 或请求失败的处理通道上错误对象上携带error.config本次请求合并后的完整配置和error.response服务端返回的错误响应网络层失败时为空这正好是编写重试决策所需的全部信息。从源码结构看拦截器链的执行有两条关键规则详见 interceptors.md请求拦截器按逆序LIFO执行响应拦截器按注册顺序FIFO执行重试是通过再次调用api(config)发起的即重新走一遍请求拦截器 → dispatchRequest → 响应拦截器的完整链路。这意味着每次重试都会再次经过所有请求拦截器——如果你的请求拦截器里做了鉴权头注入、请求打点等逻辑重试时这些逻辑会重新执行一次设计时需要保证其幂等性。二、基础重试捕获错误状态码并重发最简单的方案是捕获特定错误状态码把原始请求原样重发有限次数完整示例继承自官方文档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; // Only retry on network errors or 5xx server errors 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.response || (status 500 status 600)error.response为空表示请求在到达 HTTP 层之前就失败了连接超时、DNS 失败、CORS 阻断等网络错误这类错误往往是瞬时的值得重试500–599服务器内部错误服务端故障恢复后重试即可能成功其他状态码如 400/404属于客户端语义错误重试不会改变结果直接Promise.reject(error)放行给调用方。config._retryCount计数器的生命周期计数状态直接挂在error.config对象上。重试通过api(config)重发时传入的是同一个已合并的配置对象_retryCount属性因此跨次重试保持有效从而实现每个请求独立计数。达到MAX_RETRIES后拒绝 Promise错误最终落到业务代码的catch中。api(config)重发在底层发生了什么从 dispatchRequest.js 的源码可以看到每次重发都会执行throwIfCancellationRequested(config)检查取消状态随后经过请求数据转换transformRequest并调用 adapter 重新发请求。由于error.config是 mergeConfig 之后的完整配置重发时 baseURL、headers、data 等原样保留不需要手动拼装新请求。一个常见误区api(config)中的config是error.config——已经合并过 defaults 的配置对象。再次api(config)时 axios 会再做一次 mergeConfig自定义字段如_retryCount、_noRetry会被保留这正是该方案依赖的行为。但如果你的请求拦截器会整体替换config 对象返回一个全新的对象自定义计数字段就会丢失重试次数控制将失效——需要保证拦截器只增改字段而不换对象。三、指数退避Exponential Backoff失败后立即重试可能进一步压垮本已吃力的服务器。指数退避让每次重试前的等待时间按 2 的幂次递增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; // Wait 200ms, 400ms, 800ms, ... before each retry const backoff 100 * 2 ** config._retryCount; await delay(backoff); return api(config); } );退避时间推导_retryCount先自增再计算因此三次重试的等待分别是100 × 2¹ 200ms、100 × 2² 400ms、100 × 2³ 800ms累计最长等待 1.4 秒。实际项目中通常会把基数100ms调大到 500ms–1s 量级并可叠加抖动jitter——在退避值上乘以一个随机因子如Math.random() * 0.5 0.5避免大量客户端在同一时刻集体重试形成重试风暴。仓库中的 MIGRATION_GUIDE.md 在 Retry Logic 一节给出了参数化写法退避公式为retryDelay * Math.pow(2, count - 1)与上述示例数学上等价可作为抽取公共工厂函数的参考。进阶将退避等待做成可取消的裸的setTimeout等待无法被AbortController打断下一节会展开这个坑。如果希望取消信号能立即终止等待可以把 delay 与 signal 竞争const abortableDelay (ms, signal) new Promise((resolve, reject) { const timer setTimeout(() { signal.removeEventListener(abort, onAbort); resolve(); }, ms); const onAbort () { clearTimeout(timer); reject(new axios.CanceledError(null, { signal })); }; signal.addEventListener(abort, onAbort, { once: true }); });将await delay(backoff)替换为await abortableDelay(backoff, config.signal)即可在退避期间被用户取消。四、处理 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 // header is in seconds : 1000; // default to 1 second await new Promise((resolve) setTimeout(resolve, waitMs)); return api(config); } );源码视角为什么用全小写的retry-afteraxios 统一把响应头解析为全小写键名的对象。Node 端由 parseHeaders.js 负责每行 header 按:切分后key.trim().toLowerCase()再存入结果对象。并且retry-after被明确列入了该文件的ignoreDuplicateOf集合parseHeaders.js即同名重复头只保留第一次出现的值——这是 Nodehttp模块行为约定的对齐。因此拦截器里读取error.response.headers[retry-after]是正确的取法写Retry-After会取到undefined。另外需要注意两点429 不计入 5xx 重试逻辑。第二节的基础重试对 429 直接 reject限流恢复必须走本节这条独立分支两者可以注册为两个拦截器共存响应拦截器 FIFO 执行注册顺序即执行顺序。Retry-After的两种取值形态。该头的标准定义允许秒数如120或 HTTP-date 时间戳如Wed, 21 Oct 2015 07:28:00 GMT。上面的示例用parseFloat处理对秒数形态有效若目标服务会返回 HTTP-date 形态应改用Date.parse(retryAfterHeader) - Date.now()并做兜底判断。五、按请求粒度关闭重试并非所有请求都适合重试——幂等的读取请求可以放心重试而非幂等的变更操作如扣款、下单重试可能造成重复副作用。文档给出的做法是在请求配置上挂一个自定义开关// Add this to your interceptor before the retry logic: if (config._noRetry) return Promise.reject(error); // Then opt out on specific calls: await api.post(/payments/charge, body, { _noRetry: true });由于自定义配置字段会随 config 对象透传并跨次重试保留与_retryCount同一机制这个开关在重试链中始终可见。MIGRATION_GUIDE.md 中的变体采用了相反语义的正向开关config.retry默认不重试、显式开启才重试两种风格可按团队偏好选择关键是决策要写在拦截器最前面在计数器、退避等逻辑之前短路。实践上更稳妥的默认策略是白名单而非黑名单只对明确幂等的场景GET、HEAD、以及带幂等键的 POST启用重试其余默认不重试。六、重试与取消AbortController的协同长等待的退避期间请求其实处于挂起状态此时用户可能主动取消。文档示例const controller new AbortController(); try { await api.get(/api/data, { signal: controller.signal }); } catch (error) { if (axios.isCancel(error)) { console.log(Request aborted by user); } } // Cancel the request (and any pending retry delay) from elsewhere: controller.abort();从 dispatchRequest.js 的源码可以看到throwIfCancellationRequested会在每次 dispatch 前检查config.signal.aborted并抛出CanceledError。由于重试通过api(config)重发时signal仍在 config 中已 abort 的信号会让后续重试在发出前就被拦截最终错误经axios.isCancel(error)可识别为取消而非普通失败。需要说明的是默认写法中abort()无法中断正在进行中的setTimeout退避等待——用户最多要等当前退避结束下一次 dispatch 时才会收到取消错误。若要求点取消立即响应应使用第三节的abortableDelay将等待与信号绑定或在上层再包一层取消状态检查。取消机制的完整说明见 cancellation.md。七、完整可落地的组合示例将上述要素组合为一个可复用的拦截器工厂合并了基础重试、退避、429 分支与_noRetry开关import axios from axios; function setupRetry(api, { maxRetries 3, baseDelay 500 } {}) { const delay (ms) new Promise((r) setTimeout(r, ms)); api.interceptors.response.use( (response) response, async (error) { const config error.config; if (!config || config._noRetry) return Promise.reject(error); config._retryCount config._retryCount ?? 0; if (config._retryCount maxRetries) return Promise.reject(error); // 决定本次等待时长 let waitMs; if (error.response?.status 429) { const header error.response.headers[retry-after]; const seconds header ? parseFloat(header) : NaN; waitMs Number.isFinite(seconds) ? seconds * 1000 : 1000; } else { const isNetworkError !error.response; const isServerError error.response error.response.status 500 error.response.status 600; if (!isNetworkError !isServerError) return Promise.reject(error); // 指数退避 50% 抖动 const attempt config._retryCount 1; waitMs baseDelay * 2 ** (attempt - 1) * (0.5 Math.random() * 0.5); } config._retryCount 1; await delay(waitMs); return api(config); } ); } const api axios.create({ baseURL: https://api.example.com }); setupRetry(api, { maxRetries: 3, baseDelay: 500 }); // 幂等读取享受重试 await api.get(/api/data); // 非幂等变更显式退出重试 await api.post(/payments/charge, body, { _noRetry: true });八、适用前提与注意事项方案定位本文全部示例属于用户态拦截器方案不是 axios 的内置配置项。axios 的请求配置中没有retries/retryDelay字段重试语义完全由你的拦截器代码定义。幂等性是第一原则重试非幂等请求无幂等键的 POST/PUT/DELETE有重复执行副作用的风险务必按第五节的开关机制管控。拦截器顺序敏感响应拦截器 FIFO 执行。若你还有日志、错误上报等其他响应拦截器重试拦截器注册得越靠前越能消化掉瞬时错误避免把重试中间态暴露给下游拦截器。计数依赖同一 config 对象任何在链路上替换整个 config 对象的请求拦截器都会导致_retryCount丢失使重试退化为无限重试每次失败都视为第一次上线前建议对拦截器链做顺序与对象替换行为的审查。环境前提示例基于当前仓库axios v1.x的浏览器/Node 通用 APIAbortController在旧环境需要自行 polyfill。以上每个环节——基础重试、指数退避、429 恢复、单请求退出、取消协同——均有 retry.md 官方文档对应章节与源码实现dispatchRequest.js、parseHeaders.js互相印证可按需裁剪后直接投入生产使用。【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考