超时不是一个数字:给网络请求分配时间预算
舒心读
18
卷中目录
网络调用里的“超时”至少有三种含义:连接没有建立、某一次请求等待太久、整个操作已经超过用户能接受的期限。只给每次请求设置同一个秒数,往往约束不住总耗时。
本篇目标: 给一次读取操作分配时间预算,并让取消覆盖响应正文的读取。

先从总期限倒推
假设一个搜索动作最多允许等待 6 秒,可以先为首次请求分配 2.2 秒、退避等待分配约 0.8 秒,再把剩余时间留给下一次尝试和收尾。这些数字只是演算示例,实际应根据接口耗时分布与产品体验调整。
重要的是每次开始之前重新计算:剩余时间 = 截止时间 − 当前时间。某次请求不能因为拿到了新的尝试次数,就重新获得完整的 6 秒。计时宜使用单调时钟,避免系统时钟调整影响持续时间。
一个可取消的单次读取
下面的 JavaScript 示例只负责单次请求。计时器直到 JSON 读取结束才释放,因此响应头返回之后,正文迟迟不结束的情况也受到约束。
async function readJson(url, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} finally {
clearTimeout(timer);
}
}
AbortController 能通知相关异步操作中止,包括 fetch 与响应体消费。取消是否能传到更深层的业务处理,仍取决于服务端实现。MDN:AbortController
取消和失败要分别处理
页面离开、用户主动取消、单次超时、服务器返回错误,最好保留不同原因。界面可以给它们不同反馈,日志也才能回答请求为什么结束。外层若还要支持用户传入的取消信号,应显式合并信号或转发取消,不能悄悄覆盖它。
客户端中止并不证明服务器没有执行。读取失败通常可以重新读取;产生外部结果的操作,应先确认幂等或查询原操作结果。否则一次看似贴心的自动重试,可能重复产生结果。
检查表
- 请求、等待、解析是否共享总期限?
- 每次尝试前是否计算剩余时间?
- 计时器是否在所有返回路径上释放?
- 用户取消后是否还会启动下一次尝试?
- 错误信息能否区分取消、超时和服务端拒绝?
可以用一个延迟返回响应头、另一个延迟返回正文的测试服务分别演练。两种情况都能按预算结束,才说明超时真正覆盖了完整的读取过程。
见字如晤