2151 字
约 7 分钟
17
S02-A26 · `100-continue`:一个"先问再发"的握手,为什么被关掉了

一个上传服务支持大文件,用户抱怨"传 2GB 视频,最后被拒'文件太大'"——2GB 已经传完,才收到 413. 而协议里本来有 Expect: 100-continue:客户端"先发头",服务端"同意再发体". 那它为什么没生效?因为"中间有一层代理"把 Expect 头"去掉了".

100-continue 是 HTTP/1.1 里"唯一实用的 1xx 响应":它把"拒绝"提前到"发 body 之前"。而它的失败模式很朴素:任何一个中间层"不转发 Expect 头""或"不返回 100",这个机制就静默失效.

"先问再发"这个比喻很贴切,但它有个"隐含成本":多一次往返——于是"每个请求都要多一个 RTT"。这也是它"只对大 body 开启"的原因:curl 的默认阈值是"1KB 以上才加 Expect",而"几百字节的 JSON"根本不值得"多一个来回"。

1. 它省的是什么

项 无 100-continue 有
先发什么 头 + 体(一起) 先头,等 100 再发体
被拒时 已传完(浪费带宽) 只传了头
往返 1 次 2 次(头 → 100 → 体)

第二项是"它的全部价值":"校验失败"能在"传输开始前"发生——**于是"2GB 的拒绝"变成"1KB 的拒绝".

"抓包"是唯一可靠的判断方式,而它只要看"两行":

// 三步"看清 100-continue"(本地可复现)
1 curl -v -T big.bin http://host/upload   → 看请求头里"Expect: 100-continue"
2 看响应:有没有"HTTP/1.1 100 Continue"这一行
3 若"没有 100 但体已发" → 客户端"等到超时",或者"中间层剥掉了 Expect"
// 判据:有 Expect、无 100 → 服务端/代理没配合

2. "静默失效"的三种原因

原因 机制
代理"去掉 Expect" 服务端"根本不知道"客户端的意图
服务端"不返回 100" 客户端"等不到",按"超时后直接发"处理
客户端"不等" 有的实现"发了头就发体"

第二项"最微妙":客户端"等 100 有超时"(常见 1 秒)——**于是"服务端想拒绝"却没跑到那一步,"客户端已经开传了".

"网关提前拒"是最便宜的一层,而它"一行配置"就够:

// 两行"提前拒"(nginx)
client_max_body_size 100m;              # 超过就直接 413(不发体)
client_body_buffer_size 128k;            # 小缓冲:头一到就能判
// 判据:2GB 请求"在发体之前"收到 413

"413 提前"与"413 事后"在"日志"上长得一样,但在"带宽账"上差得很远:"2GB × 每天 50 次失败上传" = 每天 100GB 白传——而"带宽"是"云上最容易被忽视的成本之一"。所以"网关限额"这件事,不只是"体验优化",也是"成本优化"。

3. "服务端"该怎么实现

做法 说明
网关"检查头后直接返回 413" 最有效(在"最外层"拒绝)
应用"读头后返回 100 或 4xx" 要"框架支持"
反代"转发 Expect" 配置项(常默认关闭)

第一项是"最实际"的:"在 nginx 层用 client_max_body_size 直接拒"——**于是"2GB 的请求"在"发体之前"就被 413 挡住.

"客户端先校验"在浏览器里是"唯一可控"的一环,而它只要三行:

// 三步"前端先拦"(上传前)
const f = input.files[0];
if (f.size > 100 * 1024 * 1024) { alert("超过 100MB"); return; }
// 再配合"后端返回的限额"渲染提示(避免硬编码)
// 判据:用户"不必等 2GB 传完"就知道结果

4. "三种边界"与"客户端行为"

客户端 行为
curl -T(大文件) 默认加 Expect: 100-continue(>1KB 时)
浏览器(XHR/fetch) 不加(由 UA 决定)
Java HttpClient 需显式 expectContinue(true)

第二项很关键:"浏览器上传"几乎"不用 100-continue"——**于是"前端上传被拒"仍然"传完才知道".

"反代剥掉 Expect"这件事,要"显式打开转发",而它"默认常是关的":

// 三步"确认转发"(按组件)
1 nginx:查"是否 proxy_pass 时保留 Expect"(部分版本默认剥)
2 网关/ LB:查"1xx 透传"开关(有的只转发 200+)
3 应用:查框架"是否自动回 100"(有些要显式开启)
// 判据:抓包里有"Expect"但服务端日志"没有该头" → 中间层剥了

"HTTP/2" 让这件事"换了形态":它"没有 Expect: 100-continue 这套沿用"——取而代之的是"流控窗口"——于是"客户端可以先发一小段、看服务端是否 RST_STREAM"。而"迁移到 h2 之后仍想'提前拒'"的人,会发现"原来那套不适用了",得"在应用层做'先查后传'(两段式 API)".

5. "让它生效"的三步

步骤 动作
1 客户端"发 Expect" 且"等 100"(带超时)
2 中间层"转发" 反代/网关"不要剥掉"
3 服务端"按需返回" 要么 100,要么"直接 4xx"

三步缺一,就"退回'传完才拒'"——而"缺哪一步"要靠"抓包"看(Expect 在不在、100 有没有回)。

"校验放在体之后"是最常见的实现错误,而它把"提前拒"变成"事后拒":

// 两种"校验位置"
1 头之后、体之前:看"Content-Length/类型/用户额度" → 可"提前拒"
2 体之后:要"读完文件"才能算哈希/扫描 → 无法提前
// 判据:能"只靠头判断"的校验,就该"放在头之后"

6. 排错:五种"上传被拒太晚"

现象 成因 处理方向
传完才 413 无 100-continue 网关"提前拒"
Expect 被剥 代理配置 打开"转发"
客户端"等不到 100" 服务端未返回 补"100 响应"
浏览器"照传不误" UA 不加 Expect 前端"先查大小"
偶发"100 后仍被拒" 校验在"体之后" 把校验"提前到头"

7. 边界:什么情况"别指望 100-continue"

场景 为什么 做法
浏览器上传 多数 UA 不发 Expect 前端"先查大小/类型"
小请求(< 1KB) 传完也无所谓 不必
HTTP/2、HTTP/3 语义不同(无 1xx 沿用) 用"帧级"控制
中间层"多且不可控" 静默失效 网关"统一限额"

判断口径一句话:"这个请求的 body,会不会"大到'传完才发现不合法'""。会(大文件、批量导入),就要"网关提前拒 + 客户端先校验";不会(表单、JSON),"传完再拒"用户也无感——**而"指望 100-continue 在所有链路上生效",是把"协议能力"当成了"部署事实".

8. 自测

  1. 100-continue 省的是什么?为什么它"只对'会被拒的请求'有价值"?
  2. 三种"静默失效"原因里,哪一项"最微妙"?客户端等 100 的超时会造成什么?
  3. 为什么"网关层直接 413"最实际?
  4. 浏览器上传为什么"多数不用 100-continue"?这对"前端体验"意味着什么?
  5. "三步生效"里缺一步会怎样?怎么判断缺了哪一步?
  6. 判断"能否指望 100-continue"的那个问题是什么?

补一个"产品"视角:"上传失败"这件事,用户能接受"被拒",不能接受"白等"——而"2GB 传完才告诉你'太大'"正是"白等"的极端形态。所以"上传体验"的优先级应当是:前端先拦 > 网关限额 > 100-continue;前两条"一定生效",第三条"看链路脸色"。

而"看得懂链路"这件事,比"记住协议特性"更能解决实际问题。

而链路"每一跳的行为",都值得"抓包看一眼"。

9. 一句话总结

100-continue 是"协议层的提前拒绝",而它的生效需要"客户端 + 中间层 + 服务端"三方配合——三方里任何一方"不配合",它就"静默退化成'传完才拒'"。于是工程上的稳妥做法是"两头兜":网关设限额(提前拒)+ 客户端先校验(不发),而 100-continue 只是"锦上添花"。

S02-A26 · `100-continue`:一个"先问再发"的握手,为什么被关掉了
https://www.clxhxhhr.top/posts/6715/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。