问题描述
同一浏览器打开 3 个 tab,分别访问同一台 server 上不同的 canvas(space),出现部分请求"卡住"的现象:agent profiles / model provider 等信息加载不出来。关闭其中 2 个 tab 后,剩下的 tab 立即恢复正常。
根因分析
-
每个 canvas 页面会对同一 origin 建立至少 2 条长期保持打开的 SSE 连接:
GET /api/canvas/:id/sync/stream(apps/server/src/modules/canvas/sync.route.ts)
GET /api/canvas/:id/external/stream(apps/server/src/modules/canvas/external.route.ts)
若该 tab 内还有正在进行的 agent 对话,apps/server/src/modules/agent/agent.route.ts 中的 SSE 流端点会再占用一条连接。这些连接均设置 Connection: keep-alive,正常情况下不会主动关闭。
-
当前 server 走明文 HTTP(非 HTTPS),浏览器无法使用 HTTP/2 多路复用,只能退回 HTTP/1.1。浏览器(Chrome/Firefox/Safari)对同一 origin 的并发 TCP 连接数硬限制为 6 条。
-
3 个 tab × 2 条 SSE = 6 条连接,正好占满浏览器对该 origin 的连接配额。此后任何 tab 发起的新请求(如 GET /api/acp/profiles、GET /api/llm/models)都会被浏览器排队等待空闲连接,而不会真正发送到网络层——因此 server 端访问日志(apps/server/data/logs/server.log)中完全看不到这些"卡住"的请求。由于 SSE 连接被设计为长期保持打开,队列可能无限期得不到释放。
-
关闭 2 个 tab 后释放 4 条连接槽位,排队请求随即发出并成功,现象消失。
复现步骨
- 部署/运行一份 Huabu server(明文 HTTP,非托管反向代理),确保监听同一 host:port。
- 用同一浏览器打开 3 个(或以上)tab,分别访问同一 server 下不同的 canvas 路径(如
/canvas/<idA>、/canvas/<idB>、/canvas/<idC>)。
- 观察其中某个 tab 的 agent profile / model provider 等信息是否长期无法加载,同时 server 访问日志中无对应请求记录。
- 关闭其他 2 个 tab 后,问题 tab 应立即恢复正常。
建议修复方向(待评估)
- 让 server 走 HTTPS(浏览器对同源 HTTP/2 连接走多路复用,不受 6 条限制)。
- 合并
sync/stream 与 external/stream 为一条 SSE/WebSocket 连接,减少每个 tab 占用的连接数。
- 使用
SharedWorker / BroadcastChannel 让同一浏览器的多个 tab 共享一条到同一 canvas 的流式连接。
环境信息
- Server 通过
pnpm start:web(scripts/start-web.mjs)以纯 HTTP 方式监听 0.0.0.0:3001。
- 访问方式:浏览器直接访问
http://<host>:3001,未经 HTTPS 反向代理。
问题描述
同一浏览器打开 3 个 tab,分别访问同一台 server 上不同的 canvas(space),出现部分请求"卡住"的现象:agent profiles / model provider 等信息加载不出来。关闭其中 2 个 tab 后,剩下的 tab 立即恢复正常。
根因分析
每个 canvas 页面会对同一 origin 建立至少 2 条长期保持打开的 SSE 连接:
GET /api/canvas/:id/sync/stream(apps/server/src/modules/canvas/sync.route.ts)GET /api/canvas/:id/external/stream(apps/server/src/modules/canvas/external.route.ts)若该 tab 内还有正在进行的 agent 对话,
apps/server/src/modules/agent/agent.route.ts中的 SSE 流端点会再占用一条连接。这些连接均设置Connection: keep-alive,正常情况下不会主动关闭。当前 server 走明文 HTTP(非 HTTPS),浏览器无法使用 HTTP/2 多路复用,只能退回 HTTP/1.1。浏览器(Chrome/Firefox/Safari)对同一 origin 的并发 TCP 连接数硬限制为 6 条。
3 个 tab × 2 条 SSE = 6 条连接,正好占满浏览器对该 origin 的连接配额。此后任何 tab 发起的新请求(如
GET /api/acp/profiles、GET /api/llm/models)都会被浏览器排队等待空闲连接,而不会真正发送到网络层——因此 server 端访问日志(apps/server/data/logs/server.log)中完全看不到这些"卡住"的请求。由于 SSE 连接被设计为长期保持打开,队列可能无限期得不到释放。关闭 2 个 tab 后释放 4 条连接槽位,排队请求随即发出并成功,现象消失。
复现步骨
/canvas/<idA>、/canvas/<idB>、/canvas/<idC>)。建议修复方向(待评估)
sync/stream与external/stream为一条 SSE/WebSocket 连接,减少每个 tab 占用的连接数。SharedWorker/BroadcastChannel让同一浏览器的多个 tab 共享一条到同一 canvas 的流式连接。环境信息
pnpm start:web(scripts/start-web.mjs)以纯 HTTP 方式监听0.0.0.0:3001。http://<host>:3001,未经 HTTPS 反向代理。