在使用`io.Copy`时,谁应该负责处理错误?

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

When responding using io.Copy, who should be responsible for the error?

问题

假设服务器需要向客户端响应一些数据,而这些数据来自本地磁盘上的一个文件。然后我们写下以下代码:

n, err := io.Copy(w, f)  // w 是 ResponseWriter,f 是 *os.File

我的理解是,io.Copy() 首先写入一个头部,然后将数据从 f 复制到 w

err 不为 nil(比如出现了 unexpected EOF 错误)时,客户端仍然会收到状态码 200,尽管响应体中包含了错误信息。

可能是本地磁盘损坏了,或者客户端的网络出了问题。我们如何确定 err 是由服务器还是客户端引起的呢?

英文:

Assume that the server needs to respond with some data to the client and the data comes from a file on a local disk. Then we write,

n, err := io.Copy(w, f)  // w is the ResponseWriter and f is the *os.File

What I'm thinking is, io.Copy() first writes a header and then copies data from f to w.

When err is not nil (say unexpected EOF), the client still gets a status code 200, although the response body contains something wrong.

Maybe the local disk is broken, or maybe the client's network is broken. How can we determine
whether the err is caused by the server or the client?

答案1

得分: 6

io.Copy在目标io.Writer上调用Write方法。http.ResponseWriterWrite方法的文档指定了这种行为:

// Write将数据作为HTTP回复的一部分写入连接。
// 如果尚未调用WriteHeader,则在写入数据之前,Write将调用WriteHeader(http.StatusOK)。
// 如果标头不包含Content-Type行,则Write会将Content-Type添加为将初始512字节写入数据传递给DetectContentType的结果。
Write([]byte) (int, error)

这意味着它将首先调用WriteHeader

// WriteHeader使用状态码发送HTTP响应头。
// 如果未显式调用WriteHeader,则对Write的第一次调用将触发隐式的WriteHeader(http.StatusOK)。
// 因此,显式调用WriteHeader主要用于发送错误代码。
WriteHeader(int)

所以是的,如果在Write操作期间发生硬盘故障,你已经写入了一个200 OK的响应,然而,如果你的响应指定了Content-Length,当你的响应长度不匹配时,客户端将知道出现了问题。

在HTTP 1.1和分块传输编码的情况下,理论上你可以在HTTP尾部的响应之后指定一个失败的头部。然而,遗憾的是,当前最常用的Web浏览器都不支持HTTP尾部。

来自@OneOfOne的贡献:io.Copy的错误不会指定是服务器还是客户端失败。

因此,我们无法确定错误应该记录为4xx还是5xx,对吗?

如果你正在记录HTTP状态头,则记录你作为响应发送给客户端的内容,而不是应该发送的内容。

英文:

io.Copy calls Write on the target io.Writer. http.ResponseWriter's documentation on the Write method specifies this behaviour:

// Write writes the data to the connection as part of an HTTP reply.
// If WriteHeader has not yet been called, Write calls WriteHeader(http.StatusOK)
// before writing the data.  If the Header does not contain a
// Content-Type line, Write adds a Content-Type set to the result of passing
// the initial 512 bytes of written data to DetectContentType.
Write([]byte) (int, error)

That means it will first call WriteHeader:

// WriteHeader sends an HTTP response header with status code.
// If WriteHeader is not called explicitly, the first call to Write
// will trigger an implicit WriteHeader(http.StatusOK).
// Thus explicit calls to WriteHeader are mainly used to
// send error codes.
WriteHeader(int)

So yes, if your HD was to fail somewhen during a Write operation you'd already have written a 200 OK response, however, if your response specifies a Content-Length the client will know something is wrong when your response's length doesn't match.

In the case of HTTP 1.1 and chunked transfer encoding you would theoretically be able to specify a failure header after the response in an HTTP trailer. Regretfully though, HTTP trailers are not supported by any of the current most used web browsers.

Contribution from @OneOfOne: io.Copy's error will not specify which end failed; if server or client.

> So as a result, we can't point out the err should be logged as 4xx or 5xx, right?

If you're logging an HTTP status header then log what you sent your client as a response; not what it should have been.

答案2

得分: 4

当直接从文件复制到响应写入器时,唯一告诉客户端出现问题的方法是发送一个不完整的响应体。

为了强制服务器发送一个不完整的响应体,在复制响应体之前指定内容长度:

 w.Header().Set("Content-Length", strconv.Itoa(fileLen))

处理程序在复制完响应体后应该简单地返回,无论是否出现错误。

服务器会检查处理程序是否写入了内容长度头中指定的字节数。如果处理程序没有写入指定数量的字节,服务器将关闭连接。

客户端可以检测到在完整的响应体被读取之前连接被关闭。许多HTTP客户端库会在这种情况下报告错误。

如果在开始写入响应之前将文件缓冲到内存中,那么可以设置响应状态码来指示错误。如果文件很大,可能不希望进行缓冲。

处理程序很难检测到io.Copy失败是由于读取文件出错还是写入客户端出错。考虑到涉及的可能代码路径(不同的操作系统、是否使用TLS、io.Copy中的可选优化等),io.Copy可能返回许多潜在的错误。这些错误甚至可能在文件和客户端错误之间不唯一。

在复制文件之前指定内容长度还有其他好处:当内容长度已知时,服务器始终使用最高效的传输编码(身份编码)。在某些操作系统上,io.Copy操作将由内核完成。

英文:

When copying directly from the file to the response writer, the only way to tell the client that something is wrong is to send an incomplete response body.

To force the server to send an incomplete response body, specify the content length before copying the body:

 w.Header().Set("Content-Length", strconv.Itoa(fileLen))

The handler should simply return after copying the body, error or not.

The server checks to see if the handler wrote the number of bytes specified in the content length header. If the handler did not write that number of bytes, then the server closes the connection.

The client can detect that the connection was closed before the complete body was read. Many HTTP client libraries will report an error in this scenario.

If you buffer the file in memory before starting to write the response, then you can set the response status code to indicate an error. If the file is large, you may not want to buffer.

It's difficult for the handler to detect if the io.Copy failed due to an error reading the file or an error writing to the client. Given the number of possible code paths involved (different OS's, TLS or not, optional optimizations in io.Copy, ...), there are many potential errors returned from io.Copy. The errors may not even be unique between file and client errors.

Specifying the content length before copying the file has additional benefits: The server always uses the most efficient transfer encoding (the identity encoding) when the content length is known. On some operating systems, the io.Copy operation will be done by the kernel.

huangapple
  • 本文由 发表于 2014年9月29日 17:45:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/26096944.html
匿名

发表评论

匿名网友

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

确定