Nginx + Uvicorn WebSocket 反向代理问题排查
这份笔记记录了我在部署 Flutter WebSocket Chat、 FastAPI / Uvicorn 和 Nginx 时遇到的连接失败问题, 以及完整的排查过程、根因与最终正确的部署方式。核心问题是 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
错误架构与根因
最初的部署结构如下:
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 握手 |
为什么会报 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;
}
}
排查步骤 Checklist
-
先确认 Nginx 对外监听的端口是谁在占用:
ss -ltnp | grep 8000 ss -ltnp | grep 8001 -
直接测试 upstream 是否能正常返回 HTTP:
curl -v http://127.0.0.1:8001/health -
再测试经由 Nginx 的外部地址:
curl -vk https://chat.szr.hk:8000/health -
检查后端启动参数是否误开 SSL:
ps aux | grep uvicorn -
查看 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 服务。