Golang如何多次读取请求体?

huangapple go评论76阅读模式
英文:

Golang read request body multiple times

问题

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

英文:

I am writing my own logginMiddleware. Basically, I need to log body of the request and the response. The problem that I faced is that when I read body, it becomes empty and I cannot read it twice.
I understand that it happens because it is of type ReadCloser. Is there a way to rewind body to the beginning?

答案1

得分: 172

检查和模拟请求体

当你首次读取请求体时,你需要将其存储起来,这样一旦处理完毕,你就可以设置一个新的io.ReadCloser作为由原始数据构建的请求体。这样,当你在链中前进时,下一个处理程序可以读取相同的请求体。

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

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

最后一个缺失的部分是将io.Reader转换为io.ReadCloser,因为bytes.Buffer没有Close()方法。为此,你可以使用ioutil.NopCloser(),它包装了一个io.Reader,并返回一个io.ReadCloser,其添加的Close()方法将是一个空操作(什么也不做)。

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

<sup>但是要注意,可能还有其他的HTTP字段,比如内容长度和校验和,如果你只修改数据,这些字段可能会变得无效。如果后续的处理程序检查这些字段,你也需要修改它们!</sup>

检查/修改响应体

如果你还想读取响应体,那么你需要包装你得到的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。你甚至可以修改数据。你对它有完全的控制权。

<sup>但是要再次注意,后续处理程序可能还会发送与响应数据相关的HTTP响应头,比如长度或校验和,如果你修改了响应数据,这些响应头也可能变得无效。</sup>

完整示例

将这些部分组合在一起,下面是一个完整的工作示例:

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
        }

        // 处理/检查请求体。你甚至可以修改它!

        // 现在设置一个新的请求体,模拟我们读取的相同数据:
        r.Body = ioutil.NopCloser(bytes.NewBuffer(body))

        // 创建一个响应包装器:
        mrw := &MyResponseWriter{
            ResponseWriter: w,
            buf:            &bytes.Buffer{},
        }

        // 调用下一个处理程序,传递响应包装器:
        handler.ServeHTTP(mrw, r)

        // 现在检查响应,并最终发送它:
        // (你也可以在发送之前修改它!)
        if _, err := io.Copy(w, mrw.buf); err != nil {
            log.Printf("Failed to send out response: %v", err)
        }
    })
}
英文:

Inspecting and mocking request body

When you first read the body, you have to store it so once you're done with it, you can set a new io.ReadCloser as the request body constructed from the original data. So when you advance in the chain, the next handler can read the same body.

One option is to read the whole body using ioutil.ReadAll(), which gives you the body as a byte slice.

You may use bytes.NewBuffer() to obtain an io.Reader from a byte slice.

The last missing piece is to make the io.Reader an io.ReadCloser, because bytes.Buffer does not have a Close() method. For this you may use ioutil.NopCloser() which wraps an io.Reader, and returns an io.ReadCloser, whose added Close() method will be a no-op (does nothing).

Note that you may even modify the contents of the byte slice you use to create the "new" body. You have full control over it.

<sup>Care must be taken though, as there might be other HTTP fields like content-length and checksums which may become invalid if you modify only the data. If subsequent handlers check those, you would also need to modify those too!</sup>

Inspecting / modifying response body

If you also want to read the response body, then you have to wrap the http.ResponseWriter you get, and pass the wrapper on the chain. This wrapper may cache the data sent out, which you can inspect either after, on on-the-fly (as the subsequent handlers write to it).

Here's a simple ResponseWriter wrapper, which just caches the data, so it'll be available after the subsequent handler returns:

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

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

Note that MyResponseWriter.Write() just writes the data to a buffer. You may also choose to inspect it on-the-fly (in the Write() method) and write the data immediately to the wrapped / embedded ResponseWriter. You may even modify the data. You have full control.

<sup>Care must be taken again though, as the subsequent handlers may also send HTTP response headers related to the response data –such as length or checksums– which may also become invalid if you alter the response data.</sup>

Full example

Putting the pieces together, here's a full working example:

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(&quot;Error reading body: %v&quot;, err)
			http.Error(w, &quot;can&#39;t read body&quot;, 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 := &amp;MyResponseWriter{
			ResponseWriter: w,
			buf:            &amp;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(&quot;Failed to send out response: %v&quot;, err)
		}
	})
}

答案2

得分: 2

我可以使用Request包中的GetBody方法。

在net/http中的source.go源代码中有以下注释:

> GetBody定义了一个可选的函数,用于返回Body的新副本。当重定向要求多次读取Body时,它用于客户端请求。仍然需要设置Body才能使用GetBody。对于服务器请求,它是未使用的。

GetBody func() (io.ReadCloser, error)

通过这种方式,您可以获取请求的Body而不使其为空。

示例:

getBody := request.GetBody
copyBody, err := getBody()
if err != nil {
    // 处理错误并返回
}
http.DefaultClient.Do(request)
英文:

I could use the GetBody from Request package.

Look this comment in source code from request.go in net/http:

>GetBody defines an optional func to return a new copy of
Body. It is used for client requests when a redirect requires
reading the body more than once. Use of GetBody still
requires setting Body.
For server requests it is unused."

GetBody func() (io.ReadCloser, error)

This way you can get the body request without make it empty.

Sample:

getBody := request.GetBody
copyBody, err := getBody()
if err != nil {
    // Do something return err
}
http.DefaultClient.Do(request)

huangapple
  • 本文由 发表于 2017年3月26日 03:36:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/43021058.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定