update: ioutil逐渐被io 取代。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package ioutil // import "io/ioutil"

func NopCloser(r io.Reader) io.ReadCloser
    NopCloser returns a ReadCloser with a no-op Close method wrapping the
    provided Reader r.

    As of Go 1.16, this function simply calls io.NopCloser.



package io // import "io"

func NopCloser(r Reader) ReadCloser
    NopCloser returns a ReadCloser with a no-op Close method wrapping the
    provided Reader r.

最近使用baloo写集成测试,遇到了个需求, 在unmarshalrespones之后(或者之前)还要再输出一次response的纯文本格式供debug参考。

即需要多次读http.Resp.Body。

问题

response.Body 只能读一次,读完之后再进行read操作就会遇到EOF error。

分析问题

模糊记得baloo在一次请求中能多次(JSON()String())读取response.Body内容。

仔细去看了下baloo的源代码,发现baloo自己在内部 封装了一个对象 http.RawResonse ,使用了 iouti.NopCloser函数重新填充了res.Body:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func readBodyJSON(res *http.Response) ([]byte, error) {
    body, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return []byte{}, err
    }

    // Re-fill body reader stream after reading it
    res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
    return body, err
}

解决方案

有了 ioutil.NopCloser函数,可以很快速的写出debugPlugin

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func debugPlugin() (p plugin.Plugin) {
    f := func(ctx *context.Context, h context.Handler) {
        res, err := ioutil.ReadAll(ctx.Response.Body)
        fmt.Println("response:", string(res), err, "response Type:", ctx.Response.Header.Get("Content-Type"))

        // should CLOSE, due to next Close method will be no-op
        ctx.Response.Body.Close() 
        ctx.Response.Body = ioutil.NopCloser(bytes.NewBuffer(res))
        h.Next(ctx)
    }
    p = plugin.NewResponsePlugin(f)
    return
}

注意:应该先 resp.Body.Close() 掉,然后重新填充reader。

因为新的Response.Body.Close()no-op操作,不会去close 之前的Body,可能会造成资源泄漏。