
本文详解 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 的独立协议通道,其握手、证书、端口均需端到端对齐。










