socket.io 经 nginx proxy 连接时报 xhr poll error,如何解决?
先修正客户端的连接路径:将地址改为 http://172.21.48.1,通过 path: "/example/socket.io" 指定 Socket.IO 请求入口。nginx 去掉 /example/ 前缀后,请求就能到达服务端默认的 /socket.io/。
xhr poll error 表示 HTTP polling 请求失败,单凭这条消息还无法确定原因。现有代码混用了 namespace 和 HTTP 请求路径,可以先从这里修正。
为什么普通 API 正常,Socket.IO 却失败?
当前客户端代码是:
io("http://172.21.48.1/example", {
query: { unique_code: String(userId) },
});
Socket.IO 会把 URL 中的 /example 解释为 namespace(命名空间)。底层握手和 polling 请求仍使用默认的 /socket.io/ 路径,请求形式类似:
http://172.21.48.1/socket.io/?EIO=4&transport=polling&...
其中 EIO 的值取决于客户端使用的协议版本。
给出的 nginx 配置中只有:
location /example/ {
...
}
/socket.io/ 不会匹配这个 location,请求可能进入其他 location,返回 404、网页内容或其他响应,最终报出 xhr poll error。
服务端的:
new Server(server);
以及:
socketIo.on("connection", ...);
使用默认请求路径和默认 namespace /,没有注册 /example namespace。即使其他代理规则成功转发了 HTTP 请求,客户端使用的 /example namespace 也需要在服务端处理。
普通 API 调用成功,只能说明对应 API 路径的转发正常,无法据此判断 /socket.io/ 是否经过了同一条规则。
推荐修改:客户端设置 path,nginx 去掉前缀
1. 修改 React Native 连接代码
连接地址只保留协议、主机和端口,将代理入口写到 path 中:
const socket = io("http://172.21.48.1", {
path: "/example/socket.io",
query: {
unique_code: String(userId),
},
});
socket.on("connect_error", (err) => {
console.log("connect_error:", err.message);
});
这样客户端连接的是默认 namespace /,HTTP 请求走 /example/socket.io/。
2. 保留 nginx 的前缀转发方式
nginx 可以继续使用原来的配置:
location /example/ {
proxy_pass http://localhost:3336/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
在这个普通前缀 location 中,nginx 会用 proxy_pass 末尾的 / 替换匹配到的 /example/:
客户端请求:
/example/socket.io/?EIO=4&transport=polling&...
转发给 Node.js:
/socket.io/?EIO=4&transport=polling&...
查询参数也会随请求转发。
如果改成不带 URI 部分的配置:
proxy_pass http://localhost:3336;
在没有其他 rewrite 的情况下,上游收到的路径会是 /example/socket.io/,与服务端默认的 /socket.io/ 不一致。
3. 服务端保持默认路径
服务端保留默认的 path 配置即可:
export const connectIo = (server: http.Server) => {
const socketIo = new Server(server);
socketIo.on("connection", (socket) => {
// 按业务条件加入房间
});
};
nginx 会改写请求路径,因此客户端和服务端的 path 字面值可以不同。请求经过代理后,实际到达服务端的路径必须与服务端配置一致。
修改 nginx 后,检查配置并重新加载:
nginx -t
nginx -s reload
如果确实需要 /example namespace
namespace 和 path 可以同时配置,各自负责不同的事情:
const socket = io("http://172.21.48.1/example", {
path: "/example/socket.io",
query: {
unique_code: String(userId),
},
});
服务端需要监听对应的 namespace:
const socketIo = new Server(server);
socketIo.of("/example").on("connection", (socket) => {
// 处理 /example namespace 的连接
});
只有业务需要独立 namespace 时才需要这样配置。如果只是为了让连接经过 nginx 的 /example/ 入口,使用前面的默认 namespace 方案即可。
修改后如何确认问题出在哪里?
先检查 polling 握手能否通过代理。使用 Engine.IO 协议版本 4 时,可执行:
curl -i 'http://172.21.48.1/example/socket.io/?EIO=4&transport=polling'
正常情况下会返回握手数据,响应体通常以 0 开头,后面包含 sid、upgrades 等字段。这只能确认底层握手通路正常,完整的 Socket.IO 业务连接是否成功还需进一步确认。使用其他协议版本时,应按实际客户端调整 EIO。
如果仍然失败,结合 nginx access log、error log 排查:
- 返回 404 或 HTML:检查请求是否进入正确的 location,以及转发后的路径是否正确。
- 返回 502:检查 nginx 所在环境能否访问
localhost:3336。如果 nginx 位于容器内,localhost指向容器自身。 - 握手成功后连接定期断开:检查
proxy_read_timeout,使其大于服务端pingInterval + pingTimeout。 - 底层握手成功,但提示
Invalid namespace:检查客户端 URL 中的 namespace 是否已在服务端注册。
现有配置已经包含 WebSocket Upgrade 请求头。xhr poll error 出现在 polling 阶段,应先检查 HTTP 请求路径能否正确转发。仅增加 Upgrade 请求头,或直接设置 transports: ["websocket"],都无法修复路径错配。
备注:内容仅供参考。
