朕喜技术(ZhenxiJS)
2026.09.10
1 分钟阅读
Nginx 反向代理 WebSocket/SSE 长连接频繁断开(升级头缺失 + proxy_read_timeout 过短)
关键词:
WebSocket connection failed、Unexpected response code: 200、InvalidStatus、HTTP 426、101 Switching Protocols、Upgrade、Connection: upgrade、proxy_read_timeout、proxy_send_timeout、code=1006、长连接被切断、Nginx 代理 WebSocket、Nginx 代理 SSE
问题背景
通过 Nginx 反向代理对外提供 WebSocket 网关时,前端报 WebSocket connection failed(或
Unexpected response code: 200、InvalidStatus: server rejected WebSocket connection: HTTP 426),
且偶尔连上后保持几秒就断开、只能不停重连。后端直连 ws://127.0.0.1:9001 正常,问题出在 Nginx 反代层。
根本原因
本例环境在 nginx.conf 的 location /ws 中同时存在两处问题:
- 缺失 WebSocket 升级头 —— 三行
proxy_set_header(Upgrade $http_upgrade、Connection "upgrade"、Host $host)被注释。Nginx 未把客户端升级握手转发给后端, 后端按普通 HTTP 处理 → 握手失败(HTTP 426 / 200)。 - 代理读/写超时过短 ——
proxy_read_timeout 2s与proxy_send_timeout 2s。 即使握手成功,空闲超过 2s Nginx 就主动关闭上游/客户端连接(客户端收到code=1006)。
复现步骤
cd scenario/2026-09-10-0000
docker compose up -d --build # 拉起 nginx + backend + app
docker compose logs -f backend # 等 "WebSocket echo server 监听 0.0.0.0:9001"
# 现象一:握手失败
docker compose exec -T app python3 /workspace/scenario/2026-09-10-0000/client_test.py --url ws://nginx/ws --mode handshake
# => [client] 握手失败: InvalidStatus: server rejected WebSocket connection: HTTP 426
# 现象二(临时补升级头、保留 2s 超时后):长连接空闲 2s 被切断
docker compose exec -T app python3 /workspace/scenario/2026-09-10-0000/client_test.py --url ws://nginx/ws --mode idle
# => [client] 连接被服务端断开: code=1006 reason=
解决办法
在 location /ws 中:
location /ws {
proxy_pass http://ws_backend;
proxy_http_version 1.1;
# ① 关键:把升级握手正确转发给后端
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
# ② 关键:长连接读/写超时调大(如 3600s)
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# ③ 推荐:关闭缓冲,数据实时透传
proxy_buffering off;
}
要点说明:
proxy_set_header Upgrade $http_upgrade;与proxy_set_header Connection "upgrade";是 Nginx 把 WebSocket 升级握手转发给后端的关键;Host头用于保证后端按预期虚拟主机处理。proxy_read_timeout决定 Nginx 在后端读空闲时等多长时间才断开,proxy_send_timeout决定写方向超时。对 WebSocket / SSE 这类长连接都要设得足够大(3600s)或按业务调整。proxy_http_version 1.1也必不可少(HTTP/1.1 才能支持 Upgrade 头)。
验证
docker compose restart nginx
# 握手 + echo
docker compose exec -T app python3 /workspace/scenario/2026-09-10-0000/client_test.py --url ws://nginx/ws --mode handshake
# => 握手成功,seq=0..9 全部回显
# 长连接空闲(保持 30s 不中断)
docker compose exec -T app python3 /workspace/scenario/2026-09-10-0000/client_test.py --url ws://nginx/ws --mode idle
# => "30s 内未收到任何数据(连接仍存活)",不再出现 2s 即断
经验与避坑
- 诊断思路:先确认后端是否健康(直连 ws 正常),再聚焦反代层;用客户端握手看返回状态码 (426 / 200 说明升级头没转发,1006 且时间点吻合说明超时被断开)。
- 排查超时:
proxy_read_timeout默认 60s,很多“几秒就断”是被显式设成了很小的值。 改完后要看是否在空闲时仍在超时那一时刻断开。 - 注意 docker 挂载只读:
nginx.conf若以:ro挂载,docker compose cp会报device or resource busy,需直接改宿主机上的配置文件再docker compose restart nginx。 - 容器内路径:
app容器working_dir=/workspace,脚本在子目录,需用完整路径调用, 或在 compose 中把PYTHONPATH指向脚本所在目录。 - 测试端与 Nginx 区分:idle 测试用
asyncio.wait_for(..., timeout=30)作为观察窗口, 协程结束时客户端主动 close(code=1006)是测试端行为,不代表 Nginx 断开;判断标准是是否在 2s 这个时间点被切断。
可复现命令
cd scenario/2026-09-10-0000
docker compose up -d --build
docker compose exec -T app python3 /workspace/scenario/2026-09-10-0000/client_test.py --url ws://nginx/ws --mode handshake
docker compose exec -T app python3 /workspace/scenario/2026-09-10-0000/client_test.py --url ws://nginx/ws --mode idle
