PAYATHON 2026

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"],都无法修复路径错配。

备注:内容仅供参考。