Apollo Server中subscriptionsUrl返回undefined及GraphQL订阅无法在Playground运行的问题求助
解决Apollo Server 3.x中订阅subscriptionsUrl undefined及协议不支持问题
我之前也碰到过一模一样的问题,Apollo Server 3.x(包括你在用的3.10.3版本)已经移除了默认内置的WebSocket订阅支持,这就是为什么你直接调用server.listen()时subscriptionsUrl返回undefined,并且客户端会提示协议不支持的核心原因。
要搞定这个问题,你需要手动创建HTTP服务器并配置WebSocket订阅服务,具体步骤如下:
1. 安装必要依赖
首先安装订阅所需的WebSocket库(优先推荐graphql-ws,因为subscriptions-transport-ws已进入维护模式):
npm install graphql-ws ws subscriptions-transport-ws
2. 修改服务器代码
你需要放弃直接使用server.listen(),转而用Express创建HTTP服务器,然后挂载Apollo Server并启动WebSocket服务。修改后的完整代码如下:
import * as dotenv from 'dotenv' dotenv.config() import './mongo.js' import express from 'express' import { createServer } from 'http' import { ApolloServer, AuthenticationError, gql, UserInputError } from 'apollo-server-express' import { PubSub } from 'graphql-subscriptions' import { WebSocketServer } from 'ws' import { createServer as createGraphQLWsServer } from 'graphql-ws/lib/use/ws' import { execute, subscribe } from 'graphql' import Person from './models/person.js' import User from './models/user.js' import jwt from 'jsonwebtoken' const { JWT_SECRET } = process.env const SUBS_EVENTS = { PERSON_ADDED: 'PERSON_ADDED' } const pubsub = new PubSub() const typeDefs = gql`...` // 保留你原有的typeDefs定义 const resolvers = {...} // 保留你原有的resolvers逻辑 // 1. 创建Express实例和HTTP服务器 const app = express() const httpServer = createServer(app) // 2. 创建Apollo Server实例,同时处理HTTP和WebSocket的上下文 const server = new ApolloServer({ typeDefs, resolvers, context: async ({ req, connection }) => { // 处理HTTP请求的认证逻辑 if (req) { const auth = req ? req.headers.authorization : null if (auth && auth.toLowerCase().startsWith(`bearer `)) { const token = auth.substring(7) const { id } = jwt.verify(token, JWT_SECRET) const currentUser = await User.findById(id).populate('friends') return { currentUser } } } // 处理WebSocket订阅的认证逻辑(从客户端连接参数中获取token) if (connection) { const auth = connection.context.authorization || '' if (auth.toLowerCase().startsWith('bearer ')) { const token = auth.substring(7) const { id } = jwt.verify(token, JWT_SECRET) const currentUser = await User.findById(id).populate('friends') return { currentUser } } } return {} } }) // 3. 启动Apollo Server并挂载到Express await server.start() server.applyMiddleware({ app }) // 4. 配置graphql-ws的WebSocket服务器 const wsServer = new WebSocketServer({ server: httpServer, path: server.graphqlPath, // 和GraphQL HTTP端点使用同一路径,比如'/graphql' }) createGraphQLWsServer({ schema: server.schema, execute, subscribe, // 订阅连接建立时,提取客户端传递的认证参数 onConnect: async (ctx) => { return { authorization: ctx.connectionParams.authorization } }, }, wsServer) // 5. 启动HTTP服务器 const PORT = process.env.PORT || 4000 httpServer.listen(PORT, () => { console.log(`Server ready at http://localhost:${PORT}${server.graphqlPath}`) console.log(`Subscriptions ready at ws://localhost:${PORT}${server.graphqlPath}`) })
3. 关键说明
- 为什么原代码不生效?:Apollo Server 3.x移除了内置的WebSocket支持,必须手动配置WebSocket服务器,所以直接调用
server.listen()不会启动订阅服务,自然没有subscriptionsUrl返回。 - 订阅认证处理:WebSocket订阅无法通过
req对象获取认证信息,需要从客户端发送的connectionParams中提取token,所以要在onConnect回调中处理并传递到上下文。 - Apollo Playground适配:Playground会自动检测同路径的WebSocket端点,只要你配置了正确的ws地址,就能正常连接并测试订阅。
可选:兼容旧版subscriptions-transport-ws协议
如果你的客户端还在使用旧的subscriptions-transport-ws协议,可以在启动HTTP服务器之前添加以下代码,实现双协议支持:
import { SubscriptionServer } from 'subscriptions-transport-ws' SubscriptionServer.create( { schema: server.schema, execute, subscribe, onConnect: async (connectionParams) => { return { authorization: connectionParams.authorization } }, }, { server: httpServer, path: server.graphqlPath, } )
这样就能同时支持两种订阅协议,适配不同版本的客户端。
内容的提问来源于stack exchange,提问作者heyAlanDev
相关产品推荐
相关产品推荐

