51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Nginx 迷思之请求缓冲与 Go 的分块传输编码

最近在自己的某个项目中频繁被用户反馈的一个文件上传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.


赞(3)
未经允许不得转载:工具盒子 » Nginx 迷思之请求缓冲与 Go 的分块传输编码