Envoy默认不透传Upgrade头且启用idle_timeout,导致WebSocket握手失败或连接中断;需显式配置upgrade_configs、禁用idle_timeout、设置独立健康端点,并调整服务端Origin校验逻辑。
envoy 能代理 websocket,但默认配置下大概率失败——不是协议不支持,而是 http 升级(upgrade)头被过滤或超时机制误杀。
Envoy 的 http_connection_manager 必须显式启用 HTTP/1.1 upgrade
Envoy 默认只处理普通 HTTP 请求,对 Upgrade: websocket 和 Connection: upgrade 这类头不做透传。必须在 listener 的 http_connection_manager 中开启 upgrade_configs:
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
upgrade_configs:
- upgrade_type: websocket漏掉这行,客户端会收到 403 或直接断连,日志里常见 no upgrade handler for 'websocket'。
常见错误现象:
- 浏览器报
WebSocket connection to 'ws://...' failed: Error during WebSocket handshake: Unexpected response code: 403 - curl 测试握手时返回 200 但无
Upgrade头 - Envoy access log 显示 403 或 503,且无 upstream 日志
上游服务健康检查不能用 HTTP GET 探活
Envoy 对 upstream 做主动健康检查(health_check)时,若用标准 http_health_check 发送 GET 请求,而你的 Go WebSocket 服务只在 /ws 路径响应 Upgrade,其他路径一律 404 或 405,就会把健康节点标为 unhealthy。
解决方式只有两个:
- 给 WebSocket 服务加一个独立的、无状态的健康端点(如
/healthz),并在 Envoy 的health_check配置中指向它 - 禁用主动健康检查,改用被动探测(
failures+max_ejection_percent),靠连接失败自动摘除
别试图让健康检查发 Upgrade 请求——Envoy 不支持自定义 upgrade 健康探针。
超时设置必须绕开 idle_timeout
WebSocket 是长连接,但 Envoy 的 idle_timeout 默认是 5 分钟。一旦连接空闲超过该值,Envoy 会主动断开,客户端收到 close frame 或 EOF。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
必须在 route 配置中显式关闭或延长:
route: timeout: 0s idle_timeout: 0s
timeout: 0s 表示禁用请求总超时;idle_timeout: 0s 才真正禁用空闲超时(注意:不是 null,也不是省略)。
容易踩的坑:
- 只设了
timeout: 0s,没设idle_timeout→ 5 分钟后仍断连 - 设成
idle_timeout: 3600s→ 看似够长,但万一网络抖动导致短暂无数据,还是可能触发 - 在 cluster 级别设了超时,却在 virtual host 或 route 级别被覆盖
Go 服务端的 upgrader.CheckOrigin 要适配 Envoy 的真实来源
如果你的 Go 服务用了 gorilla/websocket,默认 CheckOrigin 只校验 Origin header 是否匹配 Host。但经 Envoy 后,Origin 是浏览器原始地址(如 https://myapp.com),而 Host 是 Envoy 的 upstream 地址(如 backend:8080),必然失败。
正确做法是信任 X-Forwarded-For 或明确放行:
upgrader.CheckOrigin = func(r *http.Request) bool {
// 允许所有,生产环境建议只放行可信域名
return true
// 或按 header 判断:
// origin := r.Header.Get("Origin")
// return origin == "https://myapp.com" || origin == "http://localhost:3000"
}别依赖 r.Host 做 Origin 校验——那只是 Envoy 转发后的内部地址,和客户端无关。
最后提醒一句:Envoy 的 WebSocket 支持是“透传型”的,它不解析帧内容,也不做消息路由。所有业务逻辑(比如鉴权、心跳、分片重装)都得由你的 Go 服务自己处理,Envoy 只负责把字节流原样搬过去。这点常被忽略,结果在网关层加了无效中间件,又回退到代码里补逻辑。

















