GO-POST请求避坑指北

每天上一当, 当当不一样

背景

某次服务上线, 观察线上日志发现一些报错,报错信息类似于 : 发送请求返回Post http://xxxxx.com(服务地址):http: ContentLength=1278 with Body length 0 , 立即回滚, 然后发现这个问题N久之前就存在, 差点吓尿.

问题定位

报错信息很明确 : 请求某一个接口时, Content-Length 是正常的, 但是 请求体中没有数据 , 查看发送请求的代码, 大概长这样:

1
2
3
4
5
6
7
8
9
10
11
httpClient := &http.Client{Timeout: 5 * time.Second}
// 演示代码, 忽略对异常的处理
req, _ := http.NewRequest("POST", url, bytes.NewReader(data))

//这里是请求其他系统,诉求是请求失败后可以重试
for requestCnt := 0; requestCnt < maxRetry; requestCnt++ {
resp, err := httpClient.Do(req)
if nil == err {
break
}
}

寥寥几行代码, 看起来正常无比, 会有啥问题?

问题产生位置 : 在for循环外实例化了client, 一次请求就成功,没有问题, 一旦进入重试逻辑, 必定会出现开头的错误.

查看源代码

阅读内置库Do函数的逻辑(net/http/client.go), 发现这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
.....
if resp, didTimeout, err = c.send(req, deadline); err != nil {
// c.send() always closes req.Body
reqBodyClosed = true
if !deadline.IsZero() && didTimeout() {
err = &httpError{
err: err.Error() + " (Client.Timeout exceeded while awaiting headers)",
timeout: true,
}
}
return nil, uerr(err)
}
....

发现请求出现异常时, 使用 uerr 处理错误信息, uerr 是函数内部定义的 匿名函数 , 其逻辑如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
uerr := func(err error) error {
// the body may have been closed already by c.send()
if !reqBodyClosed {
req.closeBody()
}
var urlStr string
if resp != nil && resp.Request != nil {
urlStr = stripPassword(resp.Request.URL)
} else {
urlStr = stripPassword(req.URL)
}
return &url.Error{
Op: urlErrorOp(reqs[0].Method),
URL: urlStr,
Err: err,
}
}

req.closeBody() 可以看出, 请求出现异常后, request body(io.ReadCloser) 已经被关闭, 这也解释了, 为什么重试请求之后, 请求体内没有数据. 关于 io.ReadCloser 这里不展开详细解释.

其实除了请求超时, 很多异常出现后, 都会使用 uerr 处理异常, 请求成功后, 也会关闭request body, 具体逻辑可自行查看源码

代码修正

问题定位后,修复代码逻辑可以说是最简单的一件事了, 如下 :

1
2
3
4
5
6
7
8
9
10
11
12
httpClient := &http.Client{Timeout: 5 * time.Second}

//这里是请求其他系统,诉求是请求失败后可以重试
for requestCnt := 0; requestCnt < maxRetry; requestCnt++ {
// 每次请创建新的请求实例
req, _ := http.NewRequest("POST", url, bytes.NewReader(data))

resp, err := httpClient.Do(req)
if nil == err {
break
}
}

en…… 一行代码的酸爽

扩展

go http请求还有另一种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func Post(url string, contentType string, body []byte, maxRetry int) (string, error) {
for requestCnt := 0; requestCnt < maxRetry; requestCnt++ {
res, err := http.Post(url, contentType, strings.NewReader(string(body)))
if err != nil {
return "", err
}
defer res.Body.Close()
content, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
return string(content), nil
}
}

如果采用这种写法, 因为每次都是新的 io.ReadCloser实例, 就不会出现 重复读取已被读过且已经关闭的 io.ReadCloser 的情况了. 但是这个方法有一个缺陷是, 无法控制client的各种参数, 最常见的就是 超时时间 的控制.

没有绝对完美的方案,只有开发过程中自行取舍, 涉及 io.ReadCloser 多留意上下文逻辑


GO-POST请求避坑指北
http://www.zhangdeman.cn/archives/d2abf96e.html
作者
白茶清欢
发布于
2021年10月19日
许可协议