Docker容器间WebSocket连接失败:4406子协议不兼容问题排查
问题
开发团队GraphQL订阅功能时,搭建API Gateway WebSocket API并通过VPC Link转发请求至ECS集群后端服务,但VPC Link会将WebSocket请求降级为HTTP。为此用ExpressJS搭建HTTP代理服务器,尝试连接基于graphql-ws的WebSocket后端,本地Docker容器(代理与后端)间无法建立WebSocket连接,触发4406错误:Subprotocol not acceptable。直接连接后端服务可正常工作,问题出在代理端。已尝试在WebSocket构造函数中指定/不指定graphql-transport-ws协议、使用自定义Docker网络,均无法解决问题。
代理端TypeScript代码:
import express from 'express'; import WebSocket from 'ws'; const app = express(); app.put('/', (req, res) => { const body = req.body ? req.body : {}; const socket = new WebSocket(wsEndpoint, { headers: { 'Sec-WebSocket-Version': 13, 'Sec-WebSocket-Key': 'Ab2SMOa5hg0YUyC1OELtyQ==', Connection: 'Upgrade', Upgrade: 'websocket', 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits', Host: 'localhost', }, protocol: 'graphql-transport-ws', }); socket.on('error', (err) => { console.log(JSON.stringify(err)); }); socket.on('open', (s: any) => { console.log('socket open'); }); socket.on('close', (code, reason) => { console.log(`socket close: ${code}, reason: ${reason.toString()}`); }); socket.on('error', (error) => { console.log('socket error'); console.log(error); }); socket.on('upgrade', (socket: any, request: any) => { console.log('socket upgrade'); console.log(request); }); res.status(200).json({ message: 'Connection established to WebSocket server', }); });
日志片段:
Proxy server listening on port 80 <-- app.listen() on port 80 socket upgrade <-- starts once I make the PUT request via Postman socket open socket close: 4406, reason: Subprotocol not acceptable
解决方案
问题根源
- 错误的请求处理逻辑:当前代理在
PUT请求中创建WebSocket连接,不符合WebSocket的握手流程——WebSocket通过GET请求加Upgrade头触发握手,而非PUT请求。 - 硬编码的握手头:手动指定
Sec-WebSocket-Key等头是错误的,该Key由发起请求的客户端生成,代理需直接转发客户端的原始头信息,不能硬写固定值。 - 子协议传递缺失:未正确转发客户端发送的
Sec-WebSocket-Protocol头,后端graphql-ws服务需确认该子协议才能建立连接。
修正后的代理代码
import express from 'express'; import { createServer } from 'http'; import WebSocket from 'ws'; const app = express(); const server = createServer(app); // Docker环境下用后端容器服务名代替localhost const WS_BACKEND_URL = 'ws://backend-service:4000/graphql'; // 处理WebSocket升级请求 server.on('upgrade', (req, socket, head) => { // 提取客户端请求中的子协议列表 const clientProtocols = req.headers['sec-websocket-protocol']?.toString().split(', ') || []; // 建立到后端的WebSocket连接,传递客户端原始头和子协议 const ws = new WebSocket(WS_BACKEND_URL, { headers: { ...req.headers, // Docker环境下覆盖Host为后端服务地址 Host: 'backend-service:4000' }, // 优先匹配客户端指定的graphql-transport-ws子协议 protocol: clientProtocols.includes('graphql-transport-ws') ? 'graphql-transport-ws' : undefined }); // 后端连接建立后,完成客户端的握手响应 ws.on('open', () => { socket.write(`HTTP/1.1 101 Switching Protocols\r\n` + `Upgrade: websocket\r\n` + `Connection: Upgrade\r\n` + `Sec-WebSocket-Accept: ${req.headers['sec-websocket-accept']}\r\n` + `Sec-WebSocket-Protocol: ${ws.protocol}\r\n\r\n`); // 建立客户端与后端的双向消息管道 socket.pipe(ws); ws.pipe(socket); }); ws.on('error', (err) => { console.error('后端WebSocket连接错误:', err); socket.destroy(err); }); socket.on('error', (err) => { console.error('客户端Socket错误:', err); ws.close(); }); }); // 保留基础路由(按需使用) app.get('/', (req, res) => { res.send('WebSocket代理服务器运行中'); }); const PORT = 80; server.listen(PORT, () => { console.log(`Proxy server listening on port ${PORT}`); });
关键修改说明
- 改用HTTP服务器的
upgrade事件处理WebSocket握手,遵循标准代理流程。 - 转发客户端所有原始请求头,确保握手信息完整传递。
- 建立客户端与后端的双向消息管道,实现实时数据转发。
- Docker环境下使用后端容器服务名代替
localhost,保证容器间网络互通。
内容的提问来源于stack exchange,提问作者resync
相关产品推荐
相关产品推荐

