Go httptrace.WroteRequest 返回错误时如何判断请求是否发出
来源:17golang原创
时间:2026-10-18 08:59:52 393浏览 收藏
用 Go 排查 HTTP 客户端偶发失败时,httptrace.ClientTrace.WroteRequest 很有价值,但它回答的只是一个窄问题:Transport 写请求及请求体这一阶段有没有遇到错误。WroteRequestInfo.Err 为 nil,只能说明写入阶段没有向 trace 报告错误;它不等于服务端已经收到请求,更不等于业务处理成功。
官方文档:https://pkg.go.dev/net/http/httptrace
判断“请求到底发生了什么”,要把WroteRequest与主流程返回的RoundTrip或Client.Do错误、响应头、GotConn和GotFirstResponseByte放在同一条证据链里。WroteRequestInfo.Err非空时可以确认写请求阶段失败,但仍不能仅凭它判断服务端是否看到了部分请求。
先区分 WroteRequest 回调与请求最终结果
ClientTrace 是客户端 Transport 暴露的观测钩子。WroteRequest 在请求和请求体写入结果可报告时被调用,参数里的 Err 是写请求阶段遇到的错误;在重试场景中,同一个逻辑请求可能出现多次回调。它不是服务端回执,也没有返回 HTTP 状态码。
| 证据 | 可以判断 | 不能单独判断 |
|---|---|---|
WroteRequestInfo.Err == nil | 当前一次写请求阶段没有报告写入错误 | 服务端已收到、已执行业务或已提交事务 |
WroteRequestInfo.Err != nil | 写请求阶段遇到错误,可能只写出了一部分 | 服务端完全没看到请求,或服务端一定没有副作用 |
GotFirstResponseByte 被调用 | 客户端观测到响应的第一个字节 | 响应内容代表业务成功 |
RoundTrip 返回响应 | Transport 返回了一个响应对象 | 状态码一定是 2xx,或业务操作没有重复执行 |

因此,标题里的“请求是否发出”最好拆成三个层次:客户端是否尝试写入、写入阶段是否报错、服务端是否收到并完成业务处理。第一层和第二层可以由 trace 辅助观察,第三层要依靠响应协议、服务端日志、业务幂等键或查询结果确认,不能由一个客户端回调越权证明。
读取 WroteRequestInfo.Err,并保留主流程错误
下面的示例把每次尝试的编号写入日志。注释里的“失败”只指写请求阶段失败;真正发起请求的代码仍然检查 client.Do 的返回值,并在响应存在时关闭响应体。
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptrace"
)
func doRequest(ctx context.Context, client *http.Client, req *http.Request, attempt int) error {
trace := &httptrace.ClientTrace{
WroteRequest: func(info httptrace.WroteRequestInfo) {
if info.Err != nil {
// 中文说明:这里只记录写请求阶段错误,不推断服务端一定未收到数据。
fmt.Printf("attempt=%d stage=wrote_request err=%v\n", attempt, info.Err)
return
}
// 中文说明:nil 只表示这次写入阶段没有报告错误,不是业务成功凭证。
fmt.Printf("attempt=%d stage=wrote_request err=nil\n", attempt)
},
}
traced := req.WithContext(httptrace.WithClientTrace(ctx, trace))
resp, err := client.Do(traced)
if err != nil {
// 中文说明:主流程错误可能来自连接、写入、读取或上下文取消,要保留它。
return fmt.Errorf("client.Do attempt %d: %w", attempt, err)
}
defer resp.Body.Close() // 中文说明:响应体由本次请求负责关闭,便于连接复用。
fmt.Printf("attempt=%d status=%s\n", attempt, resp.Status)
return nil
}
这里有两个容易写错的地方。第一,不能在 WroteRequest 回调里直接把 nil 当成“请求成功”,因为响应读取和业务处理还没有发生。第二,不能丢掉 client.Do 的错误;只打印 trace 日志会让连接失败、写入失败、响应头超时和上下文取消混成同一种现象。
把连接、写入和响应阶段放在同一条证据链
单独观察 WroteRequest 往往不够。排障时可以同时记录连接是否复用、是否完成请求写入、是否收到响应首字节和主流程最终错误。这样能回答“失败发生在连接前、写入中,还是已经收到响应后”。
trace := &httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) {
// 中文说明:记录连接是否复用,帮助区分连接建立问题和请求阶段问题。
fmt.Printf("stage=got_conn reused=%t was_idle=%t\n", info.Reused, info.WasIdle)
},
WroteRequest: func(info httptrace.WroteRequestInfo) {
// 中文说明:保留 Err 原值,避免把部分写出误判成完全未发送。
fmt.Printf("stage=wrote_request err=%v\n", info.Err)
},
GotFirstResponseByte: func() {
// 中文说明:首字节只说明客户端看到了响应开始,不代表业务状态成功。
fmt.Println("stage=got_first_response_byte")
},
}
建议为每个逻辑请求携带稳定的 request id,并为每次尝试增加 attempt 字段。不要只按日志出现顺序拼接事件:回调可能在请求完成前后由不同 goroutine 执行,且重试会让一个逻辑请求对应多个写入结果。

写入失败时,为什么不能立即安全重试
如果 WroteRequestInfo.Err 非空,常见反应是立刻再次发送。这个动作对查询类幂等请求通常更容易接受,但对创建订单、扣库存、发送通知等操作就可能重复产生副作用。写入错误并不能证明服务端完全没有收到字节:连接可能在服务端读取到部分请求后才断开,或者客户端在写完请求后、读取响应前遇到错误。
Go 的 Transport 会在满足条件时对某些幂等请求做底层重试,但业务层不能把这种行为扩展成“所有 POST 都能重发”。需要业务层重试时,至少先明确下面三项:
- 操作是否天然幂等,例如重复读取、按唯一键查询或带明确幂等键的写操作。
- 服务端是否支持幂等键,并能把相同键映射到同一次业务结果。
- 错误发生在连接获取、请求写入、响应读取还是响应已返回之后;不同阶段的重复风险不同。
func shouldRetry(method string, err error, idempotencyKey string) bool {
if err == nil {
return false
}
// 中文说明:只把明确的幂等方法作为默认安全候选,业务写操作需要额外协议。
switch method {
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
return true
default:
// 中文说明:有幂等键也不能跳过服务端契约确认,这里仅返回“需要业务判断”。
return idempotencyKey != "" && serverSupportsIdempotency()
}
}
这里的 serverSupportsIdempotency 只是业务策略占位,不是标准库函数。真正实现时要让服务端和客户端共同约定键的生命周期、冲突返回和结果保存方式。
一份可落地的排障清单
- 为逻辑请求记录 request id,并为每次重试记录 attempt。
- 同时记录
GotConn、WroteRequestInfo.Err、GotFirstResponseByte和Client.Do的最终错误。 - 看到
WroteRequestInfo.Err == nil时,只把它标记为“写入阶段未报错”,不要写成“服务端处理成功”。 - 看到非空错误时,保留原始错误和请求方法;对非幂等操作先确认服务端是否可能已经执行,再决定是否重试。
- 正式环境不要长期打开高噪声 trace;用短时采样、采样率和脱敏字段控制日志量。
常见疑问
WroteRequest 的 Err 为 nil,能说明请求已经发到网卡吗?
不能。它说明 Transport 的写请求阶段没有报告错误,但网络发送、服务端读取、代理转发和业务处理仍可能在之后失败。
WroteRequest 的 Err 非空,服务端一定没有收到请求吗?
不一定。写入可能部分完成,服务端或中间代理可能已经看到了一部分数据。因此非幂等操作不能只凭这个错误自动重试。
为什么还要记录 GotFirstResponseByte?
它能把“请求已经进入响应阶段”和“还停留在连接或写入阶段”区分开,但它仍然只是响应开始的客户端观测,不能替代状态码和业务结果判断。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习