Expect 100-continue是HTTP/1.1协议中一个容易被忽视的请求头,它的作用是让客户端在发送较大请求体之前,先探询服务端是否愿意接收。正常流程是客户端先发请求头,带上Expect: 100-continue,服务端如果允许,返回100 Continue这个中间响应,客户端再继续把请求体发出去。这个机制本身是为了节省带宽,但在Nginx反向代理的场景下,一旦回源环节处理不好,就可能出现请求长时间挂起、最终504超时的问题。本文就来详细拆解这个问题的成因和处理办法。

一、Expect 100-continue机制到底是怎么工作的
要理解Nginx回源时的异常,得先把这个协议机制搞明白。HTTP客户端(比如curl、Java的HttpClient、Python的requests配合特定参数)在准备发送较大请求体时,如果启用了这个机制,会先只发送请求行和请求头,其中包含Expect: 100-continue,然后停下来等待。服务端收到后有两种选择:一种是返回100 Continue,客户端随后发送请求体;另一种是直接返回最终响应,比如413 Request Entity Too Large或417 Expectation Failed,客户端就不用再发送请求体了,从而避免了白白传输大量数据。
协议还规定,如果服务端在一定时间内没有返回100,客户端不应该无限等待,通常在等待1秒左右(curl的默认行为)后会直接把请求体发出去。这个兜底行为很关键,很多情况下问题被掩盖正是因为客户端最终自己把数据发出去了,只是整体耗时多了一段等待期。用curl可以很容易复现这个行为:
curl -v -H "Expect: 100-continue" -d @large_file.bin http://backend.example.internal/upload
执行上面的命令,在verbose输出里能看到客户端发出请求头后有一段停顿,如果服务端不支持,会在大约1秒后看到客户端继续发送body。这段停顿在生产环境会被放大成明显的接口延迟。
二、Nginx回源时对Expect头的处理行为
Nginx作为反向代理时,对Expect头的处理经历过变化。在较老的版本中,Nginx会把客户端的Expect: 100-continue头原样透传给后端上游服务器,同时Nginx自己也会在收到完整请求头后立刻向客户端响应100 Continue,让客户端把请求体发过来。这就产生了一个微妙的问题:后端收到的请求里带着Expect头,如果后端是一个不识别该头的简单HTTP服务,它可能不返回100,也不返回最终响应,而是等在那里要读请求体,双方互相等待,形成僵局,直到某一侧超时。
在Nginx 1.21.0之后的版本中,ngx_http_proxy_module对回源请求的处理做了调整,默认情况下回源时不再向客户端发送100 Continue(除非后端先回了100),一定程度上让交互更符合协议。但这并不意味着所有问题都消失了,实际生产中常见的坑有这几类:
- 后端是不支持HTTP/1.1_expectation的旧服务或自研TCP服务,收到Expect头后行为异常;
- 中间还有一层LB(比如某些硬件负载均衡)对100响应处理不当,导致响应丢失;
- 客户端等待超时后重试,引发重复提交等业务层面的问题。
排查时可以先在Nginx上开启详细的访问日志和错误日志,观察回源请求的耗时分布:
log_format upstream_time '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'upstream: $upstream_addr '
'connect:$upstream_connect_time '
'header:$upstream_header_time '
'total:$upstream_response_time';
access_log /var/log/nginx/access.log upstream_time;
error_log /var/log/nginx/error.log debug;如果日志里header时间明显偏大,接近1秒或某个固定值,基本可以确认是100-continue交互环节出了问题。
三、解决方案:清理回源请求头
最直接有效的做法是在代理配置中显式把这个头清理掉,让后端收到的是一个干净的、不带Expect的请求。Nginx的proxy_set_header支持将某个头设为空值来达到移除的效果:
location /upload {
proxy_pass http://backend_cluster;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_set_header Expect "";
}注意这里把Expect设为空字符串是关键,写成别的值没有意义。同时建议显式声明proxy_http_version 1.1,配合Connection ""启用到上游的长连接,避免HTTP/1.0回源带来的额外问题。这个配置生效后,后端不会再看到Expect头,自然也就不会出现双向等待的僵局。
有一个容易搞混的点需要说明:在nginx内置变量层面,还可以通过proxy_set_header配合条件判断来精细控制。如果只想针对特定路径清理,可以借助map:
map $request_uri $expect_value {
default "100-continue";
~^/upload/ "";
}
server {
listen 80;
location / {
proxy_pass http://backend_cluster;
proxy_set_header Expect $expect_value;
}
}这种写法的好处是灵活,能按业务路径区分对待,对那些确实依赖100-continue机制做流量控制的后端(比如某些对象存储网关)保持原有行为。
四、客户端与服务端的协同优化
除了在Nginx侧处理,从源头减少这个头的产生也是值得做的。很多客户端库默认只在请求体超过一定阈值(curl是1024字节)时才主动加Expect头。如果业务上确认请求体不会大到需要这种保护,可以主动关闭。以curl为例,使用-H 'Expect:'即可移除;Java的Apache HttpClient 4.x可以通过设置HttpPost的参数禁用expect-continue,5.x则通过RequestConfig来控制;Python的requests库本身不发送Expect头,一般不用担心。
# curl禁用expect-continue curl -v -H "Expect:" -d @large_file.bin http://your-domain.com/upload
服务端这边,如果Nginx前面还有CDN或其他代理层,也要逐层确认对100 Continue的处理。特别是有些WAF设备会把100响应当成异常中间态丢弃,导致客户端一直收不到100,只能靠超时兜底。这类问题的典型表现是:所有经过该链路的POST大请求都固定慢一秒左右,用tcpdump抓包一眼就能看出来。
抓包验证是最终确认手段,在Nginx服务器上对回源网卡抓包:
tcpdump -i eth0 -A -s 0 'tcp port 8080 and host 10.0.0.20' -w expect.pcap
用Wireshark打开抓包文件,过滤http协议,观察请求头发出后到100响应(或请求体发出)之间的时间差,就能准确定位是哪一层的交互出了问题。
五、总结与排查清单
Expect 100-continue本身是一个合理的协议优化,问题出在链路上各组件对它的支持不一致。处理这类问题的思路可以归纳为三步:第一,通过Nginx日志中的$upstream_header_time和抓包确认问题确实存在于100-continue交互环节;第二,在Nginx回源配置中用proxy_set_header Expect ""清理该头,这是见效最快的手段;第三,视情况调整客户端发送策略,并在后端和中间设备层面做适配。
最后提醒一点,修改proxy_set_header相关配置后记得用nginx -t验证配置再reload,并且观察一段时间日志确认upstream_header_time是否回落到正常水平。如果清理头之后问题依旧,那就要把注意力转向后端服务本身的读超时设置,比如Tomcat的connectionTimeout、Go服务的ReadHeaderTimeout,很多时候两个问题会叠加出现,需要逐一排除。
附:常见客户端禁用Expect头的方式
为了方便查阅,这里汇总一下常见客户端的处理方式。curl加-H "Expect:";Apache HttpClient 4.x设置CoreProtocolPNames.USE_EXPECT_CONTINUE为false;HttpClient 5.x在RequestConfig中设置setExpectContinueEnabled(false);OkHttp默认不发送该头;Go的net/http默认也不会主动添加。掌握这些细节,在排查跨语言调用的大请求延迟问题时会省去很多弯路。
NginxExpect 100-continue回源配置修改时间:2026-09-09 03:24:47