页首Html代码

返回顶部

Go语言: 如何让 request.Body 可以多次读取

起因: 困惑

使用了go的http服务后, 发现 request.Body 居然只能读取一次,第二次读取数据为nil.

比如我在gin的服务器中, 先加入了accessLog,需要进行parseForm() 但是后续居然读不到数据.

所以打算深入分析一下,然后简单的解决下这个问题,再优化一下.

分析下源代码

request.Body的类型为: Body io.ReadCloser

	// Body is the request's body.
	//
	// For client requests, a nil body means the request has no                  	对于客户端请求,nil 正文表示请求没有
	// body, such as a GET request. The HTTP Client's Transport                  	正文,例如 GET 请求。HTTP 客户端的传输
	// is responsible for calling the Close method.                              	负责调用 Close 方法。
	//                                                                           	//
	// For server requests, the Request Body is always non-nil                   	对于服务器请求,请求正文始终为非 nil
	// but will return EOF immediately when no body is present.                  	但会在body没有提供数据时立即返回EOF。
	// The Server will close the request body. The ServeHTTP                     	服务器将关闭请求正文。The ServeHTTP
	// Handler does not need to.                                                 	处理程序不需要。
	//                                                                           	//
	// Body must allow Read to be called concurrently with Close.                	正文必须允许读取与关闭同时调用。
	// In particular, calling Close should unblock a Read waiting                	特别是,调用 Close 应取消阻止读取等待
	// for input.                                                                	用于输入。
	Body io.ReadCloser

可以看到 这就是一个 reader+closer 接口,真实的数据可以打印出来是:

log.Printf("c.Request.Body is %T", c.Request.Body)

结果为:
   c.Request.Body is *http.body

http.body是个未导出的类型

// body turns a Reader into a ReadCloser.
// Close ensures that the body has been fully read
// and then reads the trailer if necessary.
type body struct {
	src          io.Reader
	hdr          any           // non-nil (Response or Request) value means read trailer
	r            *bufio.Reader // underlying wire-format reader for the trailer
	closing      bool          // is the connection to be closed after reading body?
	doEarlyClose bool          // whether Close should stop early

	mu         sync.Mutex // guards following, and calls to Read and Close
	sawEOF     bool
	closed     bool
	earlyClose bool   // Close called and we didn't read to the end of src
	onHitEOF   func() // if non-nil, func to call when EOF is Read
}

好家伙 , 里面 src 又是一个 io.Reader 大部分又是 io.LimitReader ,算了不分析这个了, 无法直接进行 reset 的, 那就写一个新的reader 就好了.

纯Reader 是没有Reset或Seek接口的,这个真是应该把 request.Body 设置为 ReadSeekCloser 这种接口 ,那样就不会有此文问题了.

解决方案 (重点)

简单点的就是 读出一个 bytes 然后再覆盖掉 request.Body即可

func readBodyAndSetBodyRepeatRead(c *gin.Context, cb func()) {
	if s, ok := c.Request.Body.(io.Seeker); ok {
		//执行读取Body的操作
		cb()
		//再次设置可读状态
		_, err := s.Seek(0, 0)
		if err == nil {
			return
		}
	}

	bs, _ := io.ReadAll(c.Request.Body)
	_ = c.Request.Body.Close()// NOTE 原始的 Body 无需手动关闭,会在 response.reqBody中自动关闭的.
	//设置可读状态
	r := bytes.NewReader(bs)
	c.Request.Body = io.NopCloser(r)
	//执行读取Body的操作
	cb()
	//再次设置可读状态
	_, _ = r.Seek(0, 0)
}

bytes.NewReader 支持进行 Seek 设置,也就是可以重置读取指针(游标)位置,如果下次再次运行,就可以直接设置了

本想着 c.Request.Body 是不是要Close()呢? 查了下,发现可以不管,因为再 Response 结束后,会关闭的. 而且,这种写法也是安全的,可以看参考资料2

至于要不要用 pool 再次优化高并发下的性能,以减少 GC, 可以参考 参考资料2,我这里 就够用了.

用法

我是在gin下测试的,换做其他库简单修改下即可,反正request 都是原生 http.* 包下的.

	readBodyAndSetBodyRepeatRead(c, func() {
                io.ReadAll(c.Request.Body) // blabla
                 // or 
		_ = c.Request.ParseForm()
	})

参考资料:
1 本人学识
2 掘金网: 如何让 gin 正确多次读取 http request body 内容

posted @ 2023-03-08 11:47  ayanmw  阅读(1319)  评论(0编辑  收藏  举报

页脚Html代码