Fastify WebSocket 连接在 HTTPS 下失败的完整解决方案

1次阅读

Fastify WebSocket 连接在 HTTPS 下失败的完整解决方案

本文详解 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:// 连接来源与页面同源(协议+域名+端口)。

? 调试技巧

  1. 验证 HTTPS 是否真正启用
    curl -I https://localhost:3000 应返回 HTTP/2 200 或 HTTP/1.1 200,且无证书警告。

  2. 测试 WSS 握手(命令行):

    # 使用 wscat(npm install -g wscat) wscat -c "wss://localhost:3000/live" --insecure  # --insecure 仅开发时跳过证书校验
  3. 检查 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 的独立协议通道,其握手、证书、端口均需端到端对齐。

text=ZqhQzanResources