广告位 · 页头(后台-模块管理-新建模块后替换此区域)

Nginx回源时如何处理Transfer-Encoding: chunked请求并记录日志?

客户端如果使用HTTP/1.1的chunked编码上传文件,Nginx反向代理回源阶段并不一定把编码原样透传。源站看到的请求头有时是Content-Length,有时又是Transfer-Encoding: chunked,而Nginx的access_log里相关长度字段也容易对不上。要理解这些现象,需要先分清Nginx对请求体的缓冲策略,以及HTTP版本对传输编码的影响。

Nginx回源时如何处理Transfer-Encoding: chunked请求并记录日志?

HTTP/1.1中的chunked编码主要解决请求体长度无法提前确定的问题,尤其适合流式上传、大文件分片写入等场景。它把请求体拆成多个数据块,每个块前用十六进制数字标记长度,接收方依次读取数据块,遇到长度为0的块表示请求体结束。这样发送方不必事先计算Content-Length,接收方也能逐块解析。

一、chunked编码与Nginx的两种请求体处理模式

在Nginx反向代理中,客户端到Nginx这一段如果使用chunked上传,Nginx会先根据自身配置决定如何处理请求体。原始HTTP报文大致如下:

POST /upload HTTP/1.1
Host: ipipp.com
Transfer-Encoding: chunked
4
wiki
5
pedia
0

示例中块长度4表示后面跟着4个字节的数据wiki,块长度5表示后面是pedia,最后的0块表示结束。接收方去掉块长度行后,还原出的完整请求体为wikipedia。对于Nginx来说,是否完整读取这个body,取决于proxy_request_buffering指令。

proxy_request_buffering默认开启,表示Nginx会先把客户端请求体完整读入内存或临时文件,然后再连接源站转发。由于此时body长度已经确定,Nginx可以给源站补上Content-Length头,不再使用chunked。相反,如果显式关闭这个指令,Nginx会边接收客户端数据边发往源站,请求体总长度未知,在HTTP/1.1回源条件下才会使用Transfer-Encoding: chunked。

还有一个关键点是proxy_http_version。Nginx默认回源使用HTTP/1.0,而HTTP/1.0不支持chunked编码。所以即使配置了proxy_request_buffering off,只要回源协议仍为HTTP/1.0,源站也不会收到chunked请求。要让源站看到chunked,通常需要同时满足proxy_http_version 1.1和proxy_request_buffering off两个条件。

二、日志变量如何反映chunked请求

Nginx默认访问日志里的body_bytes_sent字段经常被误解。它表示发送给客户端的响应体字节数,并不是客户端上传请求体的大小。对于只返回状态码204或者304的接口,这个值可能为0,与请求是否chunked没有直接关系。真正和请求体长度相关的是request_length和content_length两个变量。

request_length记录Nginx从客户端实际读取到的完整请求长度,包括请求行、请求头和请求体。如果客户端使用chunked上传,Nginx在完整接收后会把这个长度计入request_length。content_length则取自客户端请求头中的Content-Length字段,chunked请求通常不携带该字段,因此content_length为空。配合http_transfer_encoding变量,就可以在访问日志中快速判断原始请求是否采用chunked编码。

log_format chunk_debug '$remote_addr "$request" '
                        'status=$status req_len=$request_length '
                        'cl=$content_length body_sent=$body_bytes_sent '
                        'te=$http_transfer_encoding';
access_log /var/log/nginx/chunk_debug.log chunk_debug;

上面的配置中,http_transfer_encoding取的是客户端请求头Transfer-Encoding的值。当客户端使用chunked上传时,日志里会出现te=chunked,同时cl为空,req_len则代表Nginx完整读取后的请求总长度。这个组合可以判断请求体是否被正常接收,也能排查客户端声明了chunked但未完成发送的情况。

需要注意的是,这些变量描述的都是客户端到Nginx这一段连接,而不是Nginx到源站这一段。Nginx发给源站的请求头不会直接出现在access_log默认字段中。如果要确认回源请求是否携带Transfer-Encoding: chunked,最直接的办法是在源站记录请求头,或者打开Nginx的debug日志查看上游请求头输出。在debug日志中搜索upstream send request header,可以看到Nginx实际发往源站的头信息。

三、回源保留chunked的配置与常见坑

如果源站能够正确解析chunked请求,并且业务需要流式上传以降低Nginx本地磁盘占用,可以将请求体缓冲关闭,并指定回源使用HTTP/1.1。一个最小配置如下:

location /upload {
    proxy_http_version 1.1;
    proxy_request_buffering off;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_pass http://backend_pool;
}

这里的proxy_set_header Connection ""用于清除客户端可能携带的Connection头,避免HTTP/1.1回源时被Connection: close提前关闭连接。proxy_pass中的backend_pool需要提前在upstream块中定义。配置生效后,当客户端使用chunked上传时,Nginx会以流式方式把分块请求转发给源站,源站看到的请求头会包含Transfer-Encoding: chunked,且通常没有Content-Length。

不过关闭请求体缓冲会带来另一个问题:Nginx无法再安全地对上游请求做重试。因为请求体可能已经部分发送给源站,一旦源站连接失败或返回错误,Nginx不能把已经发送的数据重新发送给另一台源站,否则可能写入重复数据。默认开启缓冲时,Nginx先把完整body缓存下来,遇到上游连接失败可以使用同一份缓存重试。因此对于允许重试的写操作,关闭缓冲需要谨慎评估一致性风险。

手动设置Transfer-Encoding头也是一个常见误区。Nginx会自动管理请求体传输编码,手动在proxy_set_header中指定Transfer-Encoding chunked可能造成与Content-Length同时存在、请求体解析错误,甚至导致源站返回400响应。通常应该通过proxy_http_version和proxy_request_buffering的组合来控制回源编码,而不是直接覆盖这个头。

四、抓包验证与源码层判断

从Nginx处理流程看,是否回源使用chunked,核心判断点在于请求体长度是否已知。开启缓冲后,Nginx在读完客户端body后能够确定长度,因此会优先使用Content-Length。关闭缓冲后,Nginx在转发前不知道body总长度,HTTP/1.1协议下只能选择分块传输。这个行为也解释了为什么同样的客户端请求,只改动proxy_request_buffering,源站日志中的Content-Length和Transfer-Encoding字段就会出现完全不同的结果。

如果线上环境不便修改源站代码,可以使用tcpdump抓取Nginx与源站之间的流量来验证。比如抓取8080端口的回源流量:

tcpdump -i eth0 -s0 -A 'tcp port 8080' -w /tmp/upstream.pcap

抓包后使用Wireshark过滤http.request,展开HTTP头部分,可以直观看到Nginx发往源站的请求头里是Content-Length还是Transfer-Encoding: chunked。对于不方便抓包的环境,也可以临时打开Nginx的debug日志,将error_log设置为debug级别,然后在日志里查找upstream send request header相关内容。这样能准确看到回源头信息,而不需要改动源站应用。

总结来说,Nginx日志中关于chunked的排查要分两段看:客户端到Nginx用http_transfer_encoding、content_length、request_length判断;Nginx到源站则要结合proxy_request_buffering与proxy_http_version判断。只有把这两段链路分开,才能避免被日志字段的表面含义误导,快速定位上传接口在回源阶段出现的长度不一致或chunked解析失败问题。

广告位 · 文章底部(后台-模块管理替换此区域)