直接答案:WebSocket 经 Nginx 反代出现 400 时,先检查是否使用 HTTP/1.1 并显式传递 Upgrade 和合适的 Connection 头;出现 502 时,重点检查上游地址、端口、协议、监听和握手响应。握手成功应看到 101,但 101 只证明升级完成,还要测试双向消息、空闲保持、代理 reload 和断线重连。不要把普通页面 200 当成 WebSocket 可用,也不要简单把所有超时改成一天。
本文面向浏览器控制台显示 WebSocket handshake 400/502、连接建立后约固定时间断开,或应用在 CDN/Nginx 后才失败的场景。Socket.IO 等库可能先用 HTTP 轮询再升级,路径和参数要按实际客户端协议检查。
本文按 2026 年 9 月可查的官方文档核对。示例命令以只读取证为主;凡涉及删除、覆盖、重启、权限、网络或数据库参数的动作,都应先确认操作对象、备份、维护窗口和带外恢复入口。不同发行版与软件版本的路径可能不同,必须先查询实际配置再改。
先用证据判断故障在哪一层
| 现象或检查点 | 它能说明什么 | 直接证据 | 不要误判为 |
|---|---|---|---|
| 握手返回 400 | 升级头、路径、Origin 或应用协议不满足 | 请求头、Nginx与应用日志 | 上游一定宕机 |
| 握手返回 502 | Nginx 无法获得有效上游响应 | error log、上游监听与直连 | 客户端浏览器版本 |
| 返回 200 而不是 101 | 请求被当普通 HTTP 或未升级 | 网络瀑布与响应头 | 连接已成功 |
| 固定 60 秒左右断开 | 空闲超时或无 ping/pong | 断开时长分布与代理配置 | 随机网络抖动 |
| 直连上游正常、域名失败 | 代理/CDN路径、TLS或规则问题 | 逐层握手对照 | 应用业务逻辑 |
这张表的用法不是“看到一个关键词就改一个参数”,而是把客户端、DNS或边缘、Web 代理、应用、数据层依次分开。至少保留故障时间、完整 URL、状态码、关键响应头、同一时刻的服务日志和一份正常样本。只有时间能够对齐,才能判断两个现象是否属于同一次故障。
六步排查与修复顺序
1. 固定真实 WebSocket URL
记录 ws/wss、主机、路径、查询参数、子协议、Origin 和认证方式。不要只说“聊天连不上”;同站点可能有多个 WebSocket 端点,普通 API 路径成功不能代表目标端点。
2. 确认上游独立可用
从 Nginx 主机直连应用监听地址,核对进程、端口、协议和路径。若上游使用 TLS,proxy_pass 协议、SNI 和证书要匹配;若容器内监听 127.0.0.1,宿主代理可能无法访问。
3. 按官方方式传递升级头
代理使用 HTTP/1.1,转发 $http_upgrade,并通过 map 为有升级请求设置 Connection: upgrade、普通请求设置 close。不要无条件相信客户端任意 hop-by-hop 头,也不要把配置放进未命中的 location。
4. 对齐路径、Host 与 Origin
检查 location 和 proxy_pass 末尾斜杠是否重写路径,上游是否按 Host 或 Origin 做允许列表。400 可能是应用主动拒绝而非 Nginx 语法错;用应用日志确认拒绝原因。
5. 设计空闲与保活预算
Nginx 默认在上游一段时间无数据时可能关闭连接。根据业务设置 proxy_read_timeout,应用使用 ping/pong 检测存活;CDN 和负载均衡也有各自上限,不能只改一层。
6. 覆盖重连与发布验收
测试握手 101、双向消息、空闲超过阈值、网络短断、令牌过期、Nginx reload 和应用滚动发布。客户端应指数退避重连并避免瞬间风暴;服务端要能清理失效连接和恢复订阅。
可复制的只读取证命令
以下命令中的域名、服务名、路径和时间范围要替换为自己的真实值。先执行查询命令并保存原始输出;不要把“命令能运行”误当成“业务已恢复”。
text
ss -lntp | grep ':3000'
curl -i --http1.1 -H 'Upgrade: websocket' -H 'Connection: Upgrade' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: SGVsbG9Xb3JsZDEyMzQ1Ng==' https://www.your-domain.tld/ws
nginx -T 2>&1 | grep -nE 'proxy_http_version|Upgrade|Connection|proxy_read_timeout|location'
curl -sv http://127.0.0.1:3000/health -o /dev/null
journalctl -u nginx --since '20 minutes ago' --no-pager
ss -lntp | grep ':3000':确认示例上游端口实际监听;替换为真实端口curl -i --http1.1 -H 'Upgrade: websocket' -H 'Connection: Upgrade' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: SGVsbG9Xb3JsZDEyMzQ1Ng==' https://www.your-domain.tld/ws:观察是否进入升级流程;完整协议测试应使用专业客户端nginx -T 2>&1 | grep -nE 'proxy_http_version|Upgrade|Connection|proxy_read_timeout|location':查实际加载的WebSocket相关配置curl -sv http://127.0.0.1:3000/health -o /dev/null:从代理主机验证上游健康路径,不等同WebSocket握手journalctl -u nginx --since '20 minutes ago' --no-pager:读取握手失败窗口内的connect、reset、timeout信息
如命令输出包含公网 IP、用户名、Cookie、令牌、数据库连接串或客户数据,分享工单前先脱敏。对比时保留时间戳、时区和命令本身,否则后来无法复现同一观察条件。
五个最容易扩大故障的误区
- 只添加 Upgrade 不设置 HTTP/1.1:上游可能仍无法完成协议升级。
- 普通首页 200 就宣布修复:WebSocket 要看到 101 并验证消息和长连接。
- 把 proxy_read_timeout 设极大:会长期保留僵尸连接,仍不能解决缺少心跳和上游故障。
- 忽略 proxy_pass 路径重写:末尾斜杠差异可能让上游收到错误路径并返回 400/404。
- 客户端无限立即重连:故障时会形成重连风暴,加重代理和应用负载。
变更、回滚与数据边界
保存 Nginx 旧配置并先运行 nginx -t。若新增 map/location 影响普通 HTTP、认证或其他站点,恢复上一配置并平滑 reload。调整长连接超时前记录当前并发与内存;如连接数或资源异常增长,先恢复旧阈值并启用受控重连。消息类业务回滚时还要核对重复投递与订阅状态,不能只看连接重新变绿。
如果只能先恢复服务、还不能确认根因,应把措施标记为“临时缓解”,例如限流、摘除单节点、切回上一配置或临时绕过一层代理。恢复后仍要在受控环境复现原因,并验证同类故障不会在下一次重启、发布或高峰重新出现。
统一验收清单
- [ ] 目标 ws/wss URL 与路径已明确
- [ ] 上游监听和直连健康
- [ ] 握手返回 101 而非 200/400/502
- [ ] Upgrade 与 Connection 头按条件转发
- [ ] Host、Origin 与认证符合应用规则
- [ ] 空闲超过预期窗口仍可用或按设计断开
- [ ] Nginx reload 后客户端能受控重连
- [ ] 连接数、内存与错误率无异常增长
验收必须同时覆盖机器内部和真实外部路径。首页返回 200 不能证明上传、长连接、登录、数据库写入或移动端都正常;同样,控制台显示“运行中”也不能证明应用已能处理请求。至少选一个正常请求、一个边界请求和一个失败请求,记录预期与实际结果。
相关内容与隔离测试入口
建议先阅读Nginx反向代理、502与504排查、云服务器端口不通,把本问题放回完整请求链路。若生产环境不适合直接试验,可查看美国弹性云服务器的当前规格与库存,搭建与生产隔离的复现环境;测试机不能替代生产备份,价格、库存和可选配置以实时产品页为准。
常见问题
为什么浏览器报 400,Nginx 配置看起来没错?
可能是应用拒绝路径、Origin、子协议或认证。查看应用日志和真实握手请求,不能只看 Nginx。
WebSocket 一定要设置 Connection upgrade 吗?
这些 hop-by-hop 头不会自动传给上游,Nginx 官方文档给出了显式配置方式。
连接每分钟断一次怎么查?
统计断开时间是否集中固定阈值,逐层核对 CDN、负载均衡、Nginx 和应用空闲超时以及 ping/pong。
Cloudflare 支持 WebSocket 吗?
支持不等于所有路径和套餐行为都相同。仍要核对端口、WAF、超时、日志和源站握手。
502 是不是 Nginx 没加 Upgrade?
不一定。502 更常说明上游连接、协议或响应失败;缺升级头也可能由应用返回 400。以错误日志为准。