HTTP、超时与重试边界
把 fetch 的使用经验扩展为可取消、有超时、能区分失败类型的服务调用。
HTTP、超时与重试边界
fetch 返回 500 时仍然是一个完成的 HTTP 响应;迁移到 Python 或 C++ 后,也必须区分 DNS、连接、超时、HTTP 状态和响应内容错误。一个可靠的服务调用还要声明超时、取消、最大响应体、重试资格和幂等性。网络不是函数调用的透明延伸,远端服务可能已经执行成功而客户端只看到了超时。
学习目标
- 能区分 DNS/连接/超时、HTTP 状态、响应格式和业务拒绝。
- 能为网络调用规定超时、取消、响应体上限、重试条件与总 deadline。
- 能用固定状态码和操作幂等性验证重试策略,而不是对所有失败重复请求。
请求成功不等于业务成功
下面这段代码只保留同一个意图,重点观察输入边界、数据流和失败语义,而不是逐字符翻译。
const response = await fetch(url, { signal: AbortSignal.timeout(3000) });
if (!response.ok) throw new Error("HTTP " + response.status);
return await response.json(); # Python
response = requests.get(url, timeout=3)
response.raise_for_status()
return response.json()
// C++
auto response = client.get(url, 3s);
if (!response.ok()) return Error::http(response.status()); 超时、取消与响应检查
超时是正常控制流的一部分。连接超时、读取超时和整体截止时间可以分别帮助定位 DNS、服务端处理或响应传输问题;目标语言的客户端 API 命名可能不同,但都应统一到自己的 ServiceError。收到响应后先检查状态码和 content type,再限制响应大小、解析 JSON 并验证字段。若调用方取消,应该把取消信号传到 HTTP 客户端,避免后台请求继续占用连接。
def fetch_prediction(client, url: str, payload: dict) -> dict:
response = client.post(url, json=payload, timeout=(2, 8))
if response.status_code == 429:
raise RetryableError("rate limited")
if response.status_code >= 500:
raise RetryableError(f"service status {response.status_code}")
response.raise_for_status()
body = response.json()
if not isinstance(body.get("label"), str):
raise ValueError("response label is missing")
return body
重试的工程边界
重试只适合幂等操作,且要有次数、总时间和退避上限。指数退避加抖动可以避免大量客户端同时再次冲击服务;对 400、认证失败和 schema 错误重试没有意义。创建订单或发送机器人动作时,要用幂等键或先查询状态,不能因为客户端超时就盲目重复执行。记录请求 id、attempt、耗时、状态和最终错误,才能区分服务慢与客户端过载。
常见错误与排错思路
最典型的错误是只捕获 HTTP 500,漏掉 DNS、连接拒绝和 JSON 解析失败;另一个是用 Promise.all 同时发出无限请求。排错时先查看请求阶段和超时类型,再核对服务端日志中的 request id,最后比较客户端重试次数与代理层重试次数。若延迟逐次变长,检查是否存在多层重试叠加或没有释放响应体。
为重试设定可检查的边界
重试不是“失败后再试一次”的通用补丁。先判断操作是否安全重复,再限制状态类别、尝试次数和总时间预算。以下纯函数只展示资格判断,实际客户端还要加入指数退避、随机抖动和服务端 Retry-After 处理。
function mayRetry({ method, status, attempt, hasIdempotencyKey }) {
const safeMethod = ["GET", "HEAD"].includes(method);
const repeatableWrite = method === "POST" && hasIdempotencyKey;
const transient = status === 429 || status === 502 || status === 503 || status === 504;
return attempt < 3 && transient && (safeMethod || repeatableWrite);
}
console.log(mayRetry({ method: "GET", status: 503, attempt: 1 })); // true
预期 GET 的暂时性 503 可在预算内重试;没有幂等键的 POST 不可重试,因为服务端可能已经执行成功,只是响应丢失。客户端必须把连接时间、等待退避和响应读取都计入同一个 deadline,避免每次尝试各自等待完整超时。
端到端验证
用 fake HTTP client 返回:200 但业务字段无效、400、429、503 和超时。检查只有符合策略的情况触发有限重试,取消后不再发新请求,响应体超限时及时终止读取。日志关联 request id、目标服务、尝试次数和错误类别;不记录授权头或完整个人数据。
重试间隔建议采用有限退避并加入随机抖动,避免大量客户端同步重试形成流量尖峰;服务端提供 Retry-After 时应遵守上限和整体 deadline。重试总时间超过调用方剩余预算就应停止,即使次数尚未达到上限。对每次尝试记录状态和等待时长,才能区分下游持续故障与单次网络抖动。
若浏览器/Node、Python 与 C++ 客户端结果不同,先对照连接超时和总 deadline 的定义,再检查自动重定向、代理与 TLS 配置。成功连接也不证明业务成功:必须检查状态码、内容类型、schema 和业务字段。
超时预算如何沿调用链传递
若入口给一个请求分配 2 秒 deadline,连接、每次读取、重试退避和响应解析都要共享这段预算,而不是每一层重新开始计时。下游调用结束时应区分“剩余时间不足”“远端返回可重试状态”和“远端结果已经成功但本地读取超时”。取消信号应沿调用栈传递,避免客户端退出后继续占用连接或计算资源。
运行验证:模拟远端不确定性
用 fake server 在固定时钟下分别返回延迟响应、部分响应体、非法 JSON、429 与 503。逐次确认 deadline、响应大小上限、重试次数和幂等键;再在重试等待期间取消调用,断言不会发出下一次请求。只有状态码和内容都符合契约时才把结果交给领域逻辑。
响应解析也是资源边界
即使状态码正确,远端仍可能返回错误的 content-type、格式损坏或异常大的响应体。先检查状态、内容类型与长度上限,再解析 JSON;流式下载要在读取过程中持续累计字节数,不能只信任对端声明的 Content-Length。解析失败应归类为协议错误,不要把原始响应内容直接展示给终端用户。
运行验证:响应与取消边界
为客户端设置最大 body bytes,在 fake server 中分别返回缺少长度头、声明长度与实际不一致、分块超限和空响应。确认超限时关闭读取并释放连接;调用方取消后不继续缓冲剩余 body。日志只保存状态码、目标服务标识、字节数和错误类别,避免泄露响应正文。
迁移练习
请完成:为一个模型服务调用设计三次以内的指数退避重试策略,列出可重试错误、总截止时间、幂等条件和日志字段。
HTTP、超时与重试边界练习
为一个模型服务调用设计三次以内的指数退避重试策略,列出可重试错误、总截止时间、幂等条件和日志字段。
给我一点提示
先列出可重试状态,再决定每次等待多久;不要重试参数错误或非幂等动作。
查看参考答案
仅对连接失败、超时、429 和临时 5xx 重试三次以内;使用 min(base * 2^attempt + jitter, max_delay),总时间不超过 deadline。请求必须幂等或带 idempotency_key,日志包含 request_id、attempt、status、elapsed_ms 和 retryable。 本节结论
服务边界的成熟度,体现在失败时仍然能解释发生了什么。完成后,请用 fake client 覆盖超时、429、500 和非法响应四条路径。
小结
HTTP 是不可靠的远端边界:超时不代表服务端一定没执行,状态成功也不保证响应满足业务契约。将 deadline、取消、体积和幂等重试一起设计,再迁移到文件、进程或消息系统的 IO 边界。
阶段共 8 节课,按顺序完成更容易建立完整的迁移模型。