Nginx + Uvicorn WebSocket 反向代理问题排查

这份笔记记录了我在部署 Flutter WebSocket ChatFastAPI / UvicornNginx 时遇到的连接失败问题, 以及完整的排查过程、根因与最终正确的部署方式。核心问题是 TLS / HTTPS 终止位置错误,导致上游协议不匹配。

问题现象

Flutter App 连接 WebSocket 时出现错误:


connection failed:
WebSocketException:
Connection was not upgraded to websocket

服务器端现象

  • /health 经由 Nginx 访问时返回 502 Bad Gateway
  • Nginx error log 出现 upstream prematurely closed connection
  • 直接访问 upstream 时出现 Empty reply from server
这类错误表面看像是 WebSocket Upgrade 配置错了, 但真正的根因可能是更底层的 HTTP / HTTPS 协议不匹配

错误架构与根因

最初的部署结构如下:


Flutter App
  │
  │ wss://chat.szr.hk:8000/ws
  ▼
Nginx (listen 8000 ssl)
  │
  │ proxy_pass http://127.0.0.1:8001/ws
  ▼
Uvicorn (启动时也开启了 SSL)

错误点

Uvicorn 启动时用了以下参数:


uvicorn main:app \
  --host 0.0.0.0 \
  --port 8001 \
  --ssl-certfile /etc/letsencrypt/live/chat.szr.hk/fullchain.pem \
  --ssl-keyfile /etc/letsencrypt/live/chat.szr.hk/privkey.pem

这意味着 Uvicorn 本身是 HTTPS server, 但 Nginx 却用:


proxy_pass http://127.0.0.1:8001;

去连接上游。也就是说:

组件 期望协议 实际情况
Nginx → 上游 HTTP 发送普通 HTTP 请求
Uvicorn HTTPS / TLS 等待 TLS 握手
根因一句话:Nginx 用 HTTP 去连一个 HTTPS 的 Uvicorn 上游,协议不匹配,所以连接被上游直接关闭。

为什么会报 502 和 WebSocket 未升级

1. 为什么会 502 Bad Gateway

当 Nginx 把普通 HTTP 请求转发到一个实际要求 TLS 握手的上游时, Uvicorn 无法解析请求,会直接断开连接。于是 Nginx 在读取 upstream 响应头时失败, 最终返回 502 Bad Gateway

2. 为什么 Flutter 报 “was not upgraded to websocket”

WebSocket 握手本质上也是先走 HTTP,再通过 Upgrade: websocket 升级成 WebSocket。 但因为请求根本没能正常到达可工作的后端 HTTP 服务,所以后端没返回 101 Switching Protocols,客户端就只能报 “没有升级成 websocket”。

3. 为什么去掉 Uvicorn 的 SSL 就好了

因为去掉后,整个链路变成了标准结构:


Flutter App
  │
  │ WSS / HTTPS
  ▼
Nginx (负责 TLS)
  │
  │ HTTP
  ▼
Uvicorn (普通 HTTP 服务)

现在协议就一致了:

  • 客户端到 Nginx:HTTPS / WSS
  • Nginx 到 Uvicorn:HTTP
  • WebSocket Upgrade 也能被正常代理

正确配置

正确的 Uvicorn 启动方式


uvicorn main:app --host 127.0.0.1 --port 8001

不要再带这些参数:


--ssl-certfile
--ssl-keyfile

正确的 Nginx 配置


server {
    listen 8000 ssl;
    server_name chat.szr.hk;

    ssl_certificate /etc/letsencrypt/live/chat.szr.hk/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/chat.szr.hk/privkey.pem;

    location /health {
        proxy_pass http://127.0.0.1:8001/health;
    }

    location /messages {
        proxy_pass http://127.0.0.1:8001/messages;
    }

    location /ws {
        proxy_pass http://127.0.0.1:8001/ws;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_read_timeout 3600;
    }
}
最佳实践是:TLS 只放在反向代理层, 后端应用通常只监听 127.0.0.1 的 HTTP 端口。

排查步骤 Checklist

  1. 先确认 Nginx 对外监听的端口是谁在占用:
    
    ss -ltnp | grep 8000
    ss -ltnp | grep 8001
  2. 直接测试 upstream 是否能正常返回 HTTP:
    
    curl -v http://127.0.0.1:8001/health
  3. 再测试经由 Nginx 的外部地址:
    
    curl -vk https://chat.szr.hk:8000/health
  4. 检查后端启动参数是否误开 SSL:
    
    ps aux | grep uvicorn
  5. 查看 Nginx 日志判断是回环、自签、还是 upstream 协议错误:
    
    sudo tail -n 100 /var/log/nginx/error.log

命令记录

观察到的问题命令


curl -vk https://chat.szr.hk:8000/health
curl -vk http://127.0.0.1:8001/health
ss -ltnp | grep 8001
ps aux | grep uvicorn
sudo tail -n 100 /var/log/nginx/error.log

修复后建议验证


sudo nginx -t
sudo systemctl reload nginx
curl -v http://127.0.0.1:8001/health
curl -vk https://chat.szr.hk:8000/health

表格速记

项目 错误状态 正确状态
客户端 → Nginx HTTPS / WSS HTTPS / WSS
Nginx → Uvicorn HTTP 连 HTTPS 上游 HTTP 连 HTTP 上游
Uvicorn 是否开 SSL 开了 不开
TLS 终止位置 Nginx + Uvicorn 双重 TLS 只在 Nginx
结果 502 / 无法升级 websocket 正常代理与升级

Quick Summary

  • 问题根因不是 Flutter,而是 Nginx 到 Uvicorn 的协议不匹配
  • Uvicorn 开了 SSL,但 Nginx 却按 HTTP upstream 去连接它。
  • 因此上游直接断开,Nginx 报 502 Bad Gateway
  • WebSocket 之所以失败,是因为请求根本没有成功完成 HTTP Upgrade
  • 正确做法是:Nginx 负责 TLS,Uvicorn 只提供本机 HTTP 服务