按照Apollo文档配置后GraphQL Subscriptions连接失败如何解决?
问题排查及解决方法
- 服务端订阅服务未正确挂载
Apollo Server v3及以上版本已不再默认自动挂载WebSocket订阅服务,原v2版本的写法不再生效,需要手动将订阅处理器挂载到HTTP server实例上。
- 服务端订阅服务未正确挂载
- WebSocket连接上下文未正确处理鉴权
你当前使用的express鉴权中间件仅能处理HTTP请求,WebSocket连接的鉴权逻辑需要单独处理。你当前的context函数直接读取req.user,订阅请求触发时req为undefined,会导致上下文异常,进而引发连接断开。
- WebSocket连接上下文未正确处理鉴权
- 客户端与服务端WebSocket协议不兼容
如果你的@apollo/client版本在v3.5.0以上,默认的WebSocketLink使用的是graphql-ws协议,而服务端默认适配的是subscriptions-transport-ws协议,两者不兼容会导致连接失败,需要两端使用统一的协议依赖。
- 客户端与服务端WebSocket协议不兼容
- 空密钥导致鉴权失败
你当前代码中的SECRET和SECRET2均为空值,JWT验证逻辑会直接抛出异常,导致请求被拦截,需要替换为实际使用的密钥值。
- 空密钥导致鉴权失败
服务端修正后核心代码
import { createServer } from 'http'; // 新增引入 // 其余原有引入保持不变 const app = express(); const httpServer = createServer(app); // 新增创建HTTP server实例 // 原有cors、addUser中间件逻辑保持不变 const server = new ApolloServer({ typeDefs, resolvers, subscriptions: { path: '/subscriptions', cors: { origin: "*" } // 新增订阅跨域配置 }, context: ({req, res, connection}) => { // 处理WebSocket连接的上下文 if (connection) { return connection.context; } // 原有HTTP请求上下文逻辑 const user = req.user; return { models, SECRET, SECRET2, user }; }, }); server.applyMiddleware({ app }); server.installSubscriptionHandlers(httpServer); // 新增挂载订阅处理器 // 替换原有app.listen为httpServer.listen models.sequelize.sync({force: true}).then(x => { httpServer.listen({ port: PORT }, () => { console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`); console.log(`🚀 Subscriptions ready at ws://localhost:${PORT}${server.subscriptionsPath}`); }); });
可选补充:WebSocket鉴权逻辑
如果需要对WebSocket连接做鉴权,可以在ApolloServer的subscriptions配置中添加onConnect钩子:
subscriptions: { path: '/subscriptions', cors: { origin: "*" }, onConnect: (connectionParams, webSocket) => { // 从连接参数中提取token const token = connectionParams.x_token || connectionParams.authorization?.replace('Bearer ', ''); if (!token) throw new Error('未授权'); // 验证token并返回用户信息,会自动注入到connection.context中 const { user } = jwt.verify(token, SECRET); return { user }; } }
对应客户端需要在WebSocketLink配置中携带token:
const wsLink = new WebSocketLink({ uri: 'ws://localhost:4000/subscriptions', options: { reconnect: true, // 新增连接参数携带token connectionParams: () => { const token = localStorage.getItem('token'); return { x_token: token }; } } });
内容的提问来源于stack exchange,提问作者milanf
相关产品推荐
相关产品推荐

