最近在自己的某个项目中频繁被用户反馈的一个文件上传bug, 测试也总是无法复现, debug 许久才找到问题根源, 以此记录一下.
项目中实现了兼容多种服务的文件上传接口的功能, 其中之一就是广为人知的 Alist .
这部分代码如下:
go
type Alist struct {
client *http.Client
token string
baseURL string
// ...
}
func (a *Alist) Save(ctx context.Context, reader io.Reader, path string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPut, a.baseURL+"/api/fs/put", reader)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", a.token)
req.Header.Set("File-Path", path)
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := a.client.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// ...
}
自从发布以来, 就收到一些用户反馈无法向 Alist 上传的问题, 但是无论是开发时的测试还是自己的"生产"环境都无法复现.
现在细说来这个问题出现的原因很简单, 根据 Alist 文档 和 issue #5319 可知 Alist 的文件上传接口不支持 chunked 模式, 必须传递 Content-Length
指定文件大小.
上面的代码里没有设置 Content-Length
, 且请求体是以 io.Reader
传递给 http.Client
, 查看 net/http 源码 就可以发现, 此时就会使用 chunked 模式传输 (其实这本来就是我想要的, 但谁知道 alist 不支持).
而 Alist 应该返回 400 错误: strconv.ParseInt: parsing "": invalid syntax
, 也就是解析不到 Content-Length
但是自己测试时却仍然可以上传成功, 是由于我的环境中使用 nginx 反向代理了 Alist , 根据 nginx 文档 可知, nginx 默认开启了请求缓冲 ( proxy_request_buffering
) .
向 nginx 上传文件时, nginx 会将请求体完整地缓冲到内存或硬盘中, 然后将设置了正确 Content-Length
头的新请求转发给后端.
Q.E.D.