每天上一当, 当当不一样
背景
某次服务上线, 观察线上日志发现一些报错,报错信息类似于 : 发送请求返回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 { 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 { 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 多留意上下文逻辑