答案:retryablehttp默认重试所有4xx错误(如401、403、404)存在严重风险,必须通过自定义CheckRetry函数禁用4xx重试,仅保留网络错误、5xx及429可重试,否则易引发资损、风控触发或限流恶化。

直接用 github.com/hashicorp/go-retryablehttp 能快速落地 HTTP 重试,但默认配置会重试 4xx 错误(比如 401 Unauthorized),线上已踩过坑——它不是开箱即用,而是“开箱即踩”,必须改掉默认行为才能进生产。
为什么 retryablehttp 默认重试 4xx 是个危险信号
它的 DefaultRetryPolicy 对所有 4xx 和 5xx 响应都返回 true,还包含 net.OpError、context.DeadlineExceeded 等。这意味着:
-
401、403、404全部进重试循环,可能触发下游风控或日志爆炸 - 对非幂等请求(如
POST /orders)重复提交,服务端没做幂等就等于资损 - 没过滤
429 Too Many Requests,反而加重限流压力
必须显式覆盖 CheckRetry,只保留真正可重试的错误类型。
如何正确配置 CheckRetry 函数
核心是三类可重试判定:网络层失败、5xx 服务端临时错误、明确的 429。其他一律跳过。
立即学习“go语言免费学习笔记(深入)”;
示例代码片段:
client := retryablehttp.NewClient()
client.RetryMax = 3
client.RetryWaitMin = 100 * time.Millisecond
client.RetryWaitMax = 400 * time.Millisecond
client.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
// 1. 网络错误或上下文取消,可重试
if err != nil {
if urlErr, ok := err.(*url.Error); ok {
if urlErr.Timeout() || strings.Contains(urlErr.Error(), "connection refused") {
return true, nil
}
}
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return false, err // 不重试已 cancel 的 ctx
}
return true, nil
}
// 2. 响应存在,只对 5xx 和 429 重试
if resp.StatusCode >= 500 && resp.StatusCode < 600 {
return true, nil
}
if resp.StatusCode == 429 {
return true, nil
}
// 3. 其他状态码(含全部 4xx)不重试
return false, nil
}
注意:resp.Body 在这里还没被读取,所以不用提前 Close();但如果用了 SetDoNotParseResponse(true),就得手动关 body。
别忽略 Transport 和 Timeout 的继承问题
retryablehttp.Client 底层仍用 http.Client,但它的 HTTPClient 字段默认是 &http.Client{Timeout: 30 * time.Second}。这会导致两个问题:
- 你自定义的
Transport(比如带连接池、TLS 配置)没传进去,白配 - 30 秒默认超时太长,可能掩盖真实链路瓶颈,尤其在微服务调用中
正确做法是显式构造并注入:
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
}
stdClient := &http.Client{
Transport: transport,
Timeout: 5 * time.Second, // 每次请求总超时
}
client.HTTPClient = stdClient
这样 Transport 配置生效,且单次请求不会卡死在慢响应上。
body 重放和幂等性不是库能解决的
retryablehttp 能自动重放 *strings.Reader、*bytes.Buffer 等可 seek 的 body,但它无法判断你的请求是否幂等。
- 对
GET、HEAD安全;但POST、PUT必须依赖服务端Idempotency-Key或业务层去重 - 若 body 来自
os.Stdin或不可 rewind 的io.Reader,重试会失败——得先用bytes.Buffer缓存再构造 request - 每次重试都会新建 request,所以
ctx也要重新绑定,不能复用原始请求里的已 cancel ctx
最常被忽略的一点:重试逻辑本身不保证安全,它只是把“临时故障”多试几次。真正的健壮性,取决于服务端是否幂等、客户端是否精准识别可重试边界、以及监控能否及时暴露重试风暴。


















