Focalboard WebSocket 连接失败排查:反向代理配置与 /ws 实时通道原理
【免费下载链接】focalboardFocalboard is an open source, self-hosted alternative to Trello, Notion, and Asana.项目地址: https://gitcode.com/GitHub_Trending/fo/focalboard
导读
Focalboard 通过 WebSocket 实现看板、卡片与属性的实时同步,当浏览器端长期无法建立 WebSocket 连接时,界面通常表现为"能打开页面但数据不更新、操作无实时反馈"。本文以官方排障指南 website/site/content/guide/websocket-errors/_index.md 为核心骨架,结合前端 webapp/src/wsclient.ts 与后端 server/ws/server.go 的源码实现,系统讲解连接失败的根因、两种部署形态下的修复方法,以及一套可复现的验证流程。读完本文,你将掌握如何用正确的 NGINX 反向代理配置让 Focalboard 的 WebSocket 通道稳定工作。
一、先定位:WebSocket 连接失败的典型表现与日志特征
Focalboard 的前端 WebSocket 客户端位于 webapp/src/wsclient.ts,其连接 URL 的构造逻辑非常关键:
const url = new URL(this.getBaseURL()) const protocol = (url.protocol === 'https:') ? 'wss:' : 'ws:' const wsServerUrl = `${protocol}//${url.host}${url.pathname.replace(/\/$/, '')}/ws`即:浏览器会以ws://或wss://协议,向与页面同源的/ws端点发起连接。后端在 server/ws/server.go 中注册了对应路由:
func (ws *Server) RegisterRoutes(r *mux.Router) { r.HandleFunc("/ws", ws.handleWebSocket) }当连接无法建立时,前端会留下以下可检索的日志特征(源码可见于 webapp/src/wsclient.ts):
WSClient websocket onerror. data: ...—— 由ws.onerror回调输出,说明 TCP/代理层握手失败;WSClient websocket onclose, code: X, reason: Y—— 由ws.onclose回调输出,code/reason 可用于区分正常关闭与异常中断;Unexpected WSClient close与Reopening websocket connection, count: N—— 说明连接被意外关闭,客户端正按reopenDelay间隔自动重连,直至达到reopenMaxRetries上限后输出Reached max websocket re-opening attempts。
服务端侧,升级握手失败时会输出ERROR upgrading to websocket(见 server/ws/server.go)。因此排查的第一步,是确认浏览器 DevTools 的 Network 面板中/ws请求是否返回101 Switching Protocols,以及上述日志的出现位置——这能快速判断问题出在客户端、反代层还是服务端。
二、根因:Web 代理没有透传 HTTP Upgrade 握手
WebSocket 连接的建立依赖 HTTP 的协议升级机制:客户端在请求头中携带Upgrade: websocket与Connection: Upgrade,服务端同意后返回101状态码,随后的通信转为全双工 WebSocket 帧。
Focalboard 官方排障文档明确指出:如果 WebSocket 持续无法连接,应首先检查 Web 代理的配置是否正确。原因在于,任何位于浏览器与 Focalboard 服务之间的代理(NGINX、Caddy、云负载均衡等),若未显式透传这两个升级头,或未将/ws路径的请求交给支持长连接的后端,浏览器与后端之间的握手就会被截断,表现为"页面正常、实时同步失效"。
Focalboard 官方指南给出的检查路线有两条,分别对应两种部署形态:
- 以 Mattermost 插件形式运行 Focalboard(见 部署形态说明);
- 以 Personal Server 独立服务运行并前置 NGINX(见 Personal Server(Ubuntu)部署文档)。
下面分别展开。
三、场景一:以 Mattermost 插件部署时检查 Mattermost 侧代理
当 Focalboard 作为 Mattermost 插件运行时,前端并不直接连接 Focalboard 自身的/ws,而是复用 Mattermost 的 WebSocket 连接。这一点在 webapp/src/wsclient.ts 中有明确分支:当this.client !== null(即插件模式)时,客户端为 Mattermost 的 WebSocket 客户端注册onConnect / onReconnect / onClose / onError四类回调,其中onClose还会以 500ms 间隔轮询底层conn.readyState,直到状态回到1(OPEN)才触发重连恢复。
后端对应实现在 server/ws/plugin_adapter.go,它通过 Mattermost 插件 API 接收WebSocketMessageHasBeenPosted等事件,消息动作统一以custom_focalboard_为前缀(如custom_focalboard_UPDATE_BLOCK,见 server/ws/adapter.go)。这意味着:
- 若 Mattermost 本身运行在某个 Web 代理之后,该代理必须同样支持 WebSocket Upgrade 透传,否则插件模式下 Focalboard 的实时同步同样会失效;
- 排查时可先直接访问 Mattermost 原生界面,确认其自身的实时事件是否正常——若 Mattermost 的 WebSocket 也不通,问题出在 Mattermost 的前置代理;若 Mattermost 正常而 Focalboard 无实时更新,再检查插件版本与订阅消息是否成功。
四、场景二:Personal Server + NGINX 的完整修复配置
对于独立部署的 Focalboard Personal Server,默认监听8000端口(该端口由config.json指定),官方推荐使用 NGINX 作为 Web 代理,将 80 端口的 HTTP 与 WebSocket 请求转发至后端。以下配置完整取自部署文档的 "Configure NGINX" 一节(见 website/site/content/docs/personal-edition/ubuntu.md),其中location ~ /ws/*块正是解决 WebSocket 连接失败的核心:
upstream focalboard { server localhost:8000; keepalive 32; } server { listen 80 default_server; server_name focalboard.example.com; location ~ /ws/* { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; client_max_body_size 50M; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Frame-Options SAMEORIGIN; proxy_buffers 256 16k; proxy_buffer_size 16k; client_body_timeout 60; send_timeout 300; lingering_timeout 5; proxy_connect_timeout 1d; proxy_send_timeout 1d; proxy_read_timeout 1d; proxy_pass http://focalboard; } location / { client_max_body_size 50M; proxy_set_header Connection ""; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Frame-Options SAMEORIGIN; proxy_buffers 256 16k; proxy_buffer_size 16k; proxy_read_timeout 600s; proxy_cache_revalidate on; proxy_cache_min_uses 2; proxy_cache_use_stale timeout; proxy_cache_lock on; proxy_http_version 1.1; proxy_pass http://focalboard; } }关键配置项逐条解析
| 配置项 | 作用 | 与 WebSocket 的关系 |
|---|---|---|
proxy_set_header Upgrade $http_upgrade; | 将客户端的Upgrade请求头原样转发给后端 | WebSocket 握手的关键,缺失则后端无法感知升级意图 |
proxy_set_header Connection "upgrade"; | 强制将Connection头改写为upgrade | 配合Upgrade头完成 101 升级,缺失则握手失败 |
proxy_read_timeout 1d; | 设置后端响应读取超时为 1 天 | WebSocket 长连接空闲时不会因默认 60s 超时被 NGINX 掐断 |
proxy_connect_timeout 1d;/proxy_send_timeout 1d; | 连接建立与发送超时同样放宽 | 避免高延迟网络下握手被中断 |
upstream ... keepalive 32; | 与后端保持 32 个空闲长连接 | 减少频繁重建 TCP 连接带来的握手抖动 |
proxy_http_version 1.1;(普通 location 块) | 使用 HTTP/1.1 与后端通信 | 普通请求块中配合清空的Connection头支持 keepalive;注意它与 WebSocket 块的Connection "upgrade"互斥,需分块配置 |
两个 location 块分工明确:/ws/*走 WebSocket 升级语义,/走常规 HTTP 缓存语义(Connection ""清空连接头以启用 keepalive)。切勿将Connection "upgrade"误用在普通请求块,或将普通块的无升级头配置套用到/ws块,这是最常见的配置错误。
启用与验证
创建配置后按顺序执行(完整步骤见 website/site/content/docs/personal-edition/ubuntu.md):
# 若存在默认站点需先移除 sudo rm /etc/nginx/sites-enabled/default # 启用 Focalboard 站点、测试配置并重载 sudo ln -s /etc/nginx/sites-available/focalboard /etc/nginx/sites-enabled/focalboard sudo nginx -t sudo /etc/init.d/nginx reload部署文档提供了两条验证命令:
curl localhost:8000 curl localhost第一条检查 Focalboard 服务是否在 8000 端口(默认)正常运行,第二条检查 NGINX 是否成功代理;两条命令应返回相同的 HTML 片段。若第二条失败,说明代理层配置有误;若两条均正常,但 WebSocket 仍报错,则需用下方诊断方法进一步确认。
五、深度诊断:从客户端日志到服务端握手
1. 浏览器侧:确认 101 状态码
打开 DevTools → Network → 筛选ws类型的请求,查看/ws请求:
- 状态为
101 Switching Protocols:代理与后端升级成功,问题不在传输层; - 状态为
200/404/502或持续(failed):说明请求被普通 HTTP 逻辑处理或代理层拒绝,即升级头未正确透传; - 结合第一节的客户端日志,可判断是
onerror(握手失败)还是onclose(连接被中途掐断,常见原因是代理超时)。
2. 服务端侧:确认升级路径
在后端 server/ws/server.go 中,握手失败会输出ERROR upgrading to websocket。若服务端持续输出该错误,说明升级请求未到达或到达时缺少正确的请求头;若没有任何该日志,则请求可能根本没被路由到/ws(例如被前置代理按静态资源处理)。
3. 订阅与鉴权链路
握手成功只是第一步。连接建立后,前端在ws.onopen中会发送AUTH认证指令(若配置了 token),随后发送SUBSCRIBE_TEAM/SUBSCRIBE_BLOCKS等订阅消息(动作常量定义见 server/ws/adapter.go)。若代理层将 WebSocket 数据帧误判为普通请求并缓冲/丢弃,会出现"连接显示已建立但收不到任何推送"的现象,此时应检查代理是否对/ws关闭了缓存与缓冲类指令。
六、补充:Docker 部署与端口映射
使用 Docker 运行 Personal Server 时(见 website/site/content/docs/personal-edition/docker.md),单条命令即可启动:
docker run -it -p 80:8000 mattermost/focalboard将宿主 80 端口映射到容器 8000 端口后,浏览器直接访问http://localhost即可,此时 WebSocket 同样经由 80 端口工作。若仍需在前置再加一层代理,请复用第四节中的/ws升级头配置;若直接暴露 8000 端口访问,则不存在代理截断问题,但仍需确认防火墙放行该端口。
七、客户端自动重连机制:为什么问题会被"掩盖"
值得一提的细节是:Focalboard 前端内置了自动重连逻辑,这在 webapp/src/wsclient.ts 中体现得很充分——ws.onclose中,只要关闭的不是主动发起的(ws === this.ws),就会按reopenDelay间隔递增重连次数并重新调用open(),达到reopenMaxRetries上限后才停止。因此配置错误时,用户看到的往往是"反复刷新仍不同步"而非直接报错,容易被误判为服务端故障。掌握第一节的日志关键字(Unexpected WSClient close、Reopening websocket connection),是快速区分"代理配置问题"与"服务端故障"的关键。
结语
Focalboard 的 WebSocket 通道是一条从浏览器/ws到后端 server/ws/server.go 的完整链路,任何一层代理对 Upgrade 握手的"改造"都会导致实时同步失效。按照官方排障指南的路线:插件部署查 Mattermost 侧代理,独立部署查 NGINX 的location ~ /ws/*升级头与超时配置,再配合本文提供的日志关键字与 curl 验证命令,即可系统化地定位并修复连接问题。
【免费下载链接】focalboardFocalboard is an open source, self-hosted alternative to Trello, Notion, and Asana.项目地址: https://gitcode.com/GitHub_Trending/fo/focalboard
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考