朕喜技术(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 中同时存在两处问题:

  1. 缺失 WebSocket 升级头 —— 三行 proxy_set_header(Upgrade $http_upgrade、 Connection "upgrade"、Host $host)被注释。Nginx 未把客户端升级握手转发给后端, 后端按普通 HTTP 处理 → 握手失败(HTTP 426 / 200)。
  2. 代理读/写超时过短 —— 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

版权声明

本文为博主原创文章,遵循 CC BY-NC-SA 4.0 许可协议。 转载请注明出处:http://localhost/blogs/2026-09-10-0000-websocket

分享这篇文章