Golang如何多次读取请求体?

104

我正在编写自己的日志中间件。基本上,我需要记录请求和响应的正文内容。我遇到的问题是当我读取正文内容时它变为空,并且我无法再次读取它。我理解这是因为它是ReadCloser类型。有没有一种方法将正文内容倒回开头?

2个回答

173

检查和模拟请求体

当您首次读取请求体时,必须将其存储,以便在完成后,您可以设置一个由原始数据构建的新的 io.ReadCloser 作为请求体。因此,当您在链中前进时,下一个处理程序可以读取相同的请求体。

一种选择是使用 ioutil.ReadAll() 读取整个请求体,它会将请求体作为字节切片返回。

您可以使用 bytes.NewBuffer() 从字节切片获取一个 io.Reader

最后缺少的一点是将 io.Reader 变成 io.ReadCloser,因为 bytes.Buffer 没有 Close() 方法。为此,您可以使用 ioutil.NopCloser() 将一个 io.Reader 包装起来,并返回一个 io.ReadCloser,其添加的 Close() 方法将是一个空操作(不执行任何操作)。

请注意,您甚至可以修改用于创建“新”请求体的字节切片的内容。您完全控制它。

但是必须小心,因为可能会有其他 HTTP 字段(如 content-length 和校验和),如果仅修改数据,则这些字段可能会变得无效。如果后续处理程序检查这些字段,则还需要修改这些字段!

检查/修改响应体

如果您还想读取响应体,则必须包装您获得的 http.ResponseWriter 并将包装器传递给链。此包装器可以缓存发送的数据,您可以在之后或在实时(随着后续处理程序对其进行写入)检查它。

这是一个简单的 ResponseWriter 包装器,它只是缓存数据,因此在后续处理程序返回后,它将可用:

type MyResponseWriter struct {
    http.ResponseWriter
    buf *bytes.Buffer
}

func (mrw *MyResponseWriter) Write(p []byte) (int, error) {
    return mrw.buf.Write(p)
}

请注意,MyResponseWriter.Write() 仅将数据写入缓冲区。您也可以选择在 Write() 方法中即时检查它,并立即将数据写入包装/嵌入的 ResponseWriter 中。您甚至可以修改数据。您有完全的控制权。
但是要小心,因为后续处理程序可能会发送与响应数据相关的 HTTP 响应标头 - 如长度或校验和 - 如果您更改响应数据,则这些标头也可能变为无效。
完整示例如下:
func loginmw(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        body, err := ioutil.ReadAll(r.Body)
        if err != nil {
            log.Printf("Error reading body: %v", err)
            http.Error(w, "can't read body", http.StatusBadRequest)
            return
        }

        // Work / inspect body. You may even modify it!

        // And now set a new body, which will simulate the same data we read:
        r.Body = ioutil.NopCloser(bytes.NewBuffer(body))

        // Create a response wrapper:
        mrw := &MyResponseWriter{
            ResponseWriter: w,
            buf:            &bytes.Buffer{},
        }

        // Call next handler, passing the response wrapper:
        handler.ServeHTTP(mrw, r)

        // Now inspect response, and finally send it out:
        // (You can also modify it before sending it out!)
        if _, err := io.Copy(w, mrw.buf); err != nil {
            log.Printf("Failed to send out response: %v", err)
        }
    })
}

1
在重新分配之前,您是否仍希望关闭初始主体? - Jeff
2
@Jeff 请求正文不需要由处理程序关闭,而是由服务器关闭。请参见在哪里放置“defer req.Body.Close()”? - icza
2
你应该限制从body中读取的数量,否则它只是一个DoS向量。请使用https://golang.org/pkg/net/http/#MaxBytesReader @icza - sztanpet
3
@sztanpet 是的,考虑因素还有很多,答案只是说明了如何检查请求和响应主体的理论。防止恶意大型请求应该在顶层处理,而不是在每个中间处理程序中处理。 - icza
7
ioutil.ReadAll 不建议用于处理 HTTP 请求体,尤其是高负载的服务器。详情请参见:https://haisum.github.io/2017/09/11/golang-ioutil-readall/ - zakaria amine
显示剩余2条评论

2
我可以使用Request包中的GetBody。请看net/http中request.go的源代码中的注释:
GetBody定义了一个可选函数,用于返回Body的新副本。当重定向要求多次读取Body时,它用于客户端请求。仍需要设置Body才能使用GetBody。对于服务器请求,它未使用。
使用方法:GetBody func() (io.ReadCloser, error) 这样就可以获取请求的Body而不使其为空。
示例:
getBody := request.GetBody
copyBody, err := getBody()
if err != nil {
    // Do something return err
}
http.DefaultClient.Do(request)

1
对于服务器请求,它是未使用的,您可以从服务器端获取正文以进行复制,否则会发生恐慌 runtime error: invalid memory address or nil pointer dereference - seanlook
嗨@seanlook,我犯了一个错误。 你需要检查getBody()返回的错误。`getBody := request.GetBody copyBody, err := getBody()if err != nil { // 做一些事情 return err }http.DefaultClient.Do(request)` - Yuri Giovani
使用golang 1.4时,GetBody函数返回nil,然后copyBody会触发一个错误。 - EdwinCab
2
据我所知,GetBody()不是一个要使用的函数,而是一个要定义的函数,对吧?根据文档,它是请求结构体中的可选字段,可以由用户代码填充。只有这样才能使用。而不是反过来。 - latitov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接