WebSocket upgrade returned 200:升级头被中间层吞掉了
WebSocket upgrade returned 200 instead of 101严重程度 High升级请求被当作普通 HTTP 请求处理并返回了 200,说明链路上某一层没有转发 Upgrade 与 Connection 头。最可能是反向代理未显式配置转发或 CDN 未开启 WebSocket 支持;第一步是按源站直连、经 CDN、经客户端的顺序逐层验证。
结论
在所有升级失败的状态码里,200 是信息量最大的那一个。
它意味着:请求到达了某个 HTTP 服务、那个服务正常处理并返回了内容——但它完全不知道你想要一个 WebSocket。你发出的 Upgrade: websocket 在半路被丢掉了,后端收到的只是一个普通的 GET 请求,于是它老老实实返回了那个路径上的常规内容。
这把嫌疑精确地指向了中间层,而不是你的网络或后端服务本身。
适用环境
- 全部平台
- 高发场景:经过 Nginx 等反向代理或 CDN 的 wss:// 服务
一分钟快速判断
connproof websocket wss://你的地址/路径按返回的状态码分流:
| 状态码 | 含义 | 对应 |
|---|---|---|
| 200 | 被当成普通 HTTP 处理 | 本页(WS-004) |
| 404 | 路径不存在 | WS-001 |
| 400 | 升级头畸形或被部分剥离 | WS-001 |
| 403 | 被策略拒绝 | WS-001 |
| 502 / 504 | 后端不可用 | HTTP-003 |
| 101 但随即断开 | 升级成功、会话被拒 | WS-002 |
200 与 404 的区别值得强调:404 说明路由不通,200 说明路由通了但协议没升级。前者查路径,后者查代理配置——方向完全不同。
这条错误代表什么
关键在于 Upgrade 和 Connection 是 HTTP 规范里的逐跳头(hop-by-hop header)。逐跳头的定义就是"只在相邻两个节点之间有效,不应被转发"——这是规范的要求,不是代理的 bug。
后果是:链路上每多一层反向代理,就多一处需要显式配置把这两个头加回去的地方。任何一层漏配,升级请求就退化成普通请求。这也解释了为什么"本地直连后端好好的,上了 CDN 就不行"是这类问题的经典剧情——多了一层,就多了一个需要配置的地方。
ConnProof 如何判断
检测器执行真实的 RFC 6455 握手,读取响应状态码。当状态码恰好是 200 时判定为 WS-004,并在证据里明确写出这个推断:
中间层判定:服务端以普通 HTTP 200 应答升级请求,说明 Upgrade / Connection 头未被转发
其他非 101 状态码仍归 WS-001,因为它们指向的是路径、鉴权或后端可用性,而不是逐跳头。
逐层定位方法
这是本页最实用的部分。按从内到外的顺序测三次,第一个失败的层就是元凶:
第一层:源站直连。 服务维护者在服务器本机或绕过所有中间层的情况下测:
connproof websocket ws://127.0.0.1:后端端口/路径这里就失败 → 后端服务本身没有正确实现 WebSocket,与代理无关。
第二层:经反向代理。 直接连代理监听的端口:
connproof websocket wss://你的域名/路径源站正常而这里返回 200 → 反向代理没有转发升级头,配置见下节。
第三层:经 CDN。 如果域名走了 CDN,上一步测的其实已经是 CDN。把域名临时解析到源站 IP 再测一次做对照:源站正常而经 CDN 返回 200 → CDN 未开启 WebSocket 支持。
普通用户做不了第 1、3 步也没关系——把第 2 步的状态码证据发给服务方,这个数字足够他们定位。
反向代理配置要点(服务维护者)
以 Nginx 为例,一个能正确转发升级请求的 location 至少需要:
location /你的路径 {
proxy_pass http://后端地址;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 300s;
}2
3
4
5
6
7
8
四个要点:proxy_http_version 1.1 是前提(HTTP/1.0 不支持升级机制);两个 proxy_set_header 负责把逐跳头重新加回去,缺一不可;proxy_read_timeout 决定空闲连接能活多久,默认 60 秒常常是"连上一分钟就断"的元凶(那属于 LONG-003)。
CDN 侧还要确认三件事:所用套餐支持 WebSocket;已为该域名显式开启;该路径已排除出缓存规则——被缓存的升级请求会稳定返回 200,症状与漏配升级头完全一致。
为什么本地测试常常发现不了这个问题
开发环境里客户端直连后端,中间一层代理都没有,升级头自然畅通无阻——问题只在部署到生产、加上反向代理和 CDN 之后才出现。这是本条错误最典型的发现路径,也是它容易被漏测的原因。
由此可以推出一条实用的验证原则:WebSocket 功能必须在完整链路上验证,本机测试通过不能作为发布依据。理想的做法是把 connproof websocket 加入部署后的检查清单,与其他冒烟测试一起跑——它只需要一条命令,却能拦住一类每次改动代理配置都可能复现的故障。
同理,任何一次涉及反向代理、CDN 或负载均衡的配置调整之后,都值得重跑一次这条检测。逐跳头是"不配置就丢失"的机制,它不会在改配置时提醒你。
如何区分本地问题和服务端问题
| 证据 | 结论 |
|---|---|
| 返回 200 | 中间层配置(服务端侧,与你的网络无关) |
| 源站直连正常、经代理 200 | 反向代理漏配 |
| 经代理正常、经 CDN 200 | CDN 未开启 WebSocket 或缓存了该路径 |
| 多设备多网络都返回 200 | 确认为服务端配置,不必再换网络 |
| 换网络后变成超时而非 200 | 那是另一类问题,转 TLS-002 思路 |
仍未解决时收集什么信息
- ConnProof 错误码(WS-004)与脱敏报告;
- 升级响应状态码 200(关键证据);
- 逐层测试的结果(哪一层开始变成 200);
- 客户端名称和版本、操作系统版本。
禁止提交:完整订阅链接、密码、验证码、私钥、Token。
重新运行检测
connproof websocket wss://你的地址/路径 --hold 5000 --format markdown --output ws-report.md修复后应看到 101、Accept 校验通过,并能保持到设定时长。
相关文档
更新记录
- 2026-07-29:规则 v1 发布。此前所有非 101 响应统一归入 WS-001,本次把 200 单独拆出——它明确指向逐跳头未被转发,排查方向与路径、鉴权类失败完全不同。