
本文详解 fastify 配合 @fastify/websocket 在启用 https(wss)时连接失败的常见原因与实战修复方案,涵盖证书配置、服务器初始化逻辑、客户端连接注意事项及调试技巧。
本文详解 fastify 配合 @fastify/websocket 在启用 https(wss)时连接失败的常见原因与实战修复方案,涵盖证书配置、服务器初始化逻辑、客户端连接注意事项及调试技巧。
在使用 Fastify 构建全栈实时应用时,@fastify/websocket 是官方推荐的 WebSocket 插件,但许多开发者在从 HTTP 切换到 HTTPS 后会遇到 ws:// 可连而 wss:// 无法建立连接的问题——服务端日志无报错、HTTPS 页面正常加载,但浏览器控制台提示 net::ERR_CONNECTION_REFUSED 或 Error during WebSocket handshake: net::ERR_SSL_PROTOCOL_ERROR。这并非插件兼容性问题,而是 HTTPS/WSS 协议协同中的典型配置疏漏。
✅ 正确初始化 Fastify 实例(关键!)
首要问题是:WebSocket 支持必须与 HTTPS 配置同步注入 Fastify 实例。你当前的代码中,@fastify/websocket 是在 https: {…} 选项外注册的,看似无害,但 Fastify 的 WebSocket 插件依赖底层 server 实例的协议能力。若 https 配置未在 fastify() 构造时传入,即使后续监听 HTTPS 端口,WebSocket 升级(upgrade)请求仍可能被降级处理或拒绝。
请统一使用以下初始化方式(推荐单实例 + 条件化配置):
const fs = require('fs'); const fastify = require('fastify'); const config = { privKey: process.env.PRIV_KEY, certKey: process.env.CERT_KEY, https: process.env.HTTPS === '1', domains: process.env.DOMAINS?.split(',') || ['*'], port: parseInt(process.env.PORT) || 3000, }; // 统一构建 Fastify 实例(HTTPS 配置必须在此处声明) const fastifyInstance = fastify({ serverTimeout: 60 * 60 * 1000, logger: true, ...(config.https && { https: { key: fs.readFileSync(config.privKey), cert: fs.readFileSync(config.certKey), // ⚠️ 生产环境建议添加 ca(如使用中间证书) // ca: fs.readFileSync('./fullchain.pem'), } }) }); // ✅ 此时再注册 CORS 和 WebSocket(顺序无关紧要,但必须在 listen() 前) fastifyInstance.register(require('@fastify/cors'), { origin: config.domains, }); fastifyInstance.register(require('@fastify/websocket')); // WebSocket 路由注册 fastifyInstance.register(async function (instance) { instance.get('/live', { websocket: true }, (connection, request) => { console.log('New WebSocket connection from:', request.socket.remoteAddress); connection.socket.on('message', (data) => { try { const message = data.toString(); console.log('Received:', message); connection.socket.send(`Echo: ${message}`); } catch (err) { console.error('Send error:', err); } }); connection.socket.on('close', () => { console.log('Connection closed'); }); }); }); // 启动监听(自动适配 http(s)) const start = async () => { try { const address = await fastifyInstance.listen({ host: '0.0.0.0', port: config.port, // ⚠️ 重要:HTTPS 模式下必须显式指定 secure: true(否则默认走 HTTP) ...(config.https && { secure: true }) }); fastifyInstance.log.info(`Server listening at ${address}`); } catch (err) { fastifyInstance.log.error(err); process.exit(1); } }; start();
? 证书要求:WSS 不接受自签名证书(除非显式信任)
你提到“证书有效,Fastify HTTPS 工作正常”,但需注意:浏览器对 wss:// 的 TLS 校验比 https:// 更严格。即使页面通过 https:// 加载,WebSocket 连接仍会独立校验证书链完整性。
-
❌ 浏览器直接拒绝未受信的自签名证书(如 OpenSSL 生成的),且不会弹出“继续访问”提示;
-
✅ 推荐开发环境使用 mkcert 生成本地可信证书:
# 安装并初始化本地 CA mkcert -install # 为 localhost 生成证书 mkcert localhost 127.0.0.1 ::1 # 输出:localhost-key.pem + localhost.pem将生成的 .pem 文件路径赋给 PRIV_KEY 和 CERT_KEY 环境变量即可。
-
? 生产环境务必使用 Let’s Encrypt(certbot)或云厂商签发的完整证书链(含 fullchain.pem),避免因缺少中间证书导致 WSS 握手失败。
? 客户端连接:协议必须严格匹配
前端连接时,务必确保 URL 协议与服务端一致:
// ✅ 正确:HTTPS 页面 → WSS 连接 const ws = new WebSocket('wss://your-domain.com/live'); // ❌ 错误:HTTPS 页面尝试 ws://(混合内容被浏览器阻止) // const ws = new WebSocket('ws://your-domain.com/live'); // Blocked by browser! // ✅ 开发环境(localhost + mkcert) const ws = new WebSocket('wss://localhost:3000/live');
? 提示:Chrome 控制台 > Application > Frames 中可查看当前页面协议,确保 wss:// 连接来源与页面同源(协议+域名+端口)。
? 调试技巧
-
验证 HTTPS 是否真正启用:
curl -I https://localhost:3000 应返回 HTTP/2 200 或 HTTP/1.1 200,且无证书警告。 -
测试 WSS 握手(命令行):
# 使用 wscat(npm install -g wscat) wscat -c "wss://localhost:3000/live" --insecure # --insecure 仅开发时跳过证书校验 -
检查 Fastify 日志级别:
启用详细日志:logger: { level: ‘trace’ },观察是否有 upgrade 请求被拦截或 websocket: false 相关 warn。
✅ 总结:WSS 连接成功的三大前提
| 要素 | 要求 | 检查方式 |
|---|---|---|
| 服务端配置 | https 选项必须在 fastify() 初始化时传入,且 @fastify/websocket 在其后注册 | 查看 fastify({ https: {…} }) 是否存在 |
| TLS 证书 | 必须为浏览器信任的证书(mkcert / Let’s Encrypt),且 cert 包含完整链 | openssl x509 -in cert.pem -text -noout | grep “Issuer” |
| 客户端连接 | URL 协议(wss://)与页面协议一致,无跨域或混合内容限制 | 浏览器 Network 标签页查看 WebSocket 请求状态码 |
遵循以上方案,99% 的 Fastify + WSS 连接问题将迎刃而解。记住:WSS 不是“HTTPS 上的 WS”,而是基于 TLS 的独立协议通道,其握手、证书、端口均需端到端对齐。