ApolloServer 4客户端无法连接订阅?问题排查求助
Apollo Server 4 订阅无法连接且上下文函数未触发问题
我已完成ApolloServer 4客户端与服务端的搭建,查询(Query)和突变(Mutation)功能正常,但订阅(Subscription)完全无法连接。查询和突变能正常传递上下文(Context),但订阅的上下文函数完全未触发。我推测可能需要某种拆分链接(split link),但已研究并尝试各类splitLink达3-4周仍未解决问题。请问我遗漏了什么?
ApolloServer 配置
import {ApolloServer} from "@apollo/server"; import {expressMiddleware} from "@apollo/server/express4"; import {ApolloServerPluginDrainHttpServer} from "@apollo/server/plugin/drainHttpServer"; import express from "express"; import http from "http"; import cors from "cors"; import {PubSub, withFilter} from "graphql-subscriptions-continued"; import {WebSocketServer} from "ws"; import {useServer} from "graphql-ws/lib/use/ws"; import {makeExecutableSchema} from "@graphql-tools/schema"; import {MongodbPubSub} from 'graphql-mongodb-subscriptions'; import {getUserIdByLoginToken} from "./utils_server/meteor-apollo-utils"; import typeDefs from "./api/schema"; import {resolvers} from "./api/resolvers"; import mongoose from "mongoose"; import bodyParser from "body-parser"; const MONGODB_URI = `mongodb://localhost:3001/meteor`; const connectToDb = async () => { await mongoose.connect(MONGODB_URI); }; await connectToDb(); console.log('🎉 Connected to database successfully'); const mongodbpubsub = new MongodbPubSub({ connectionDb: mongoose.connections[0].db }); // Create the schema, which will be used separately by ApolloServer and // the WebSocket server. const schema = makeExecutableSchema({typeDefs, resolvers}); const app = express(); const httpServer = http.createServer(app); const wsServer = new WebSocketServer({ path: "/graphql", server: httpServer, }); const serverCleanUp = useServer({schema}, wsServer); const apolloServer = new ApolloServer({ schema, plugins: [ ApolloServerPluginDrainHttpServer({httpServer}), { async serverWillStart() { return { async drainServer() { await serverCleanUp.dispose(); } } } } ], }); await apolloServer.start(); app.use( "/graphql", cors({ origin: "http://localhost:3000", credentials: true, }), express.json(), expressMiddleware(apolloServer, { context: async (ctx, msg, args) => { let token = ctx.req.headers['token'] let user = null; let userId = null; try { if ((!!token) && (token !== "null")) { [user, userId] = await getUserIdByLoginToken(token); } } catch (error) { console.log('context: ', error) } let clientIp = ''; try { clientIp = ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress; } catch { console.log("Couldn't get clientIp in ctx.req") } return { user: user, userId: userId, clientIp: clientIp, pubsub: mongodbpubsub }; } }) ); await new Promise((resolve) => httpServer.listen({port: 4000}, resolve)); const PORT = 4000; console.log(`Server is now running on http://localhost:${PORT}/graphql`);
ApolloClient 配置
import { ApolloClient, HttpLink, InMemoryCache, ApolloLink, ApolloProvider } from "@apollo/client"; // Create an HttpLink pointing to your GraphQL endpoint const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' }); // Middleware to add custom headers const customHeadersMiddleware = new ApolloLink((operation, forward) => { // Define your custom headers const customHeaders = { "token": localStorage.getItem("Meteor.loginToken") }; // Use operation.setContext to add the custom headers to the request operation.setContext(({ headers }) => ({ headers: { ...headers, ...customHeaders, }, })); return forward(operation); }); // Combine the middleware with the HttpLink const apolloClient = new ApolloClient({ link: customHeadersMiddleware.concat(httpLink), cache: new InMemoryCache(), }); export {apolloClient};
问题排查与解决方案
1. 客户端缺少WebSocket链接与拆分逻辑
当前客户端仅配置了HttpLink,而订阅依赖WebSocket协议通信,必须添加WebSocketLink并通过split链接区分操作类型:
修改后的客户端配置:
import { ApolloClient, HttpLink, InMemoryCache, ApolloLink, split, ApolloProvider } from "@apollo/client"; import { getMainDefinition } from '@apollo/client/utilities'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { createClient } from 'graphql-ws'; // HTTP链接 const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' }); // WebSocket链接,传递认证token到服务端 const wsLink = new GraphQLWsLink(createClient({ url: 'ws://localhost:4000/graphql', connectionParams: { token: localStorage.getItem("Meteor.loginToken") }, })); // 自定义头部中间件 const customHeadersMiddleware = new ApolloLink((operation, forward) => { const customHeaders = { "token": localStorage.getItem("Meteor.loginToken") }; operation.setContext(({ headers }) => ({ headers: { ...headers, ...customHeaders, }, })); return forward(operation); }); // 拆分链接:查询/突变走HTTP,订阅走WebSocket const splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ); }, wsLink, customHeadersMiddleware.concat(httpLink), ); const apolloClient = new ApolloClient({ link: splitLink, cache: new InMemoryCache(), }); export {apolloClient};
2. 服务端未配置WebSocket的上下文处理
你当前的useServer仅传入了schema,没有配置上下文函数,导致订阅的上下文完全不触发。需要给useServer添加context选项,且WebSocket的认证信息需从connectionParams获取(而非HTTP请求头):
修改服务端中useServer的配置:
const serverCleanUp = useServer({ schema, context: async (ctx) => { let token = ctx.connectionParams?.token; let user = null; let userId = null; try { if ((!!token) && (token !== "null")) { [user, userId] = await getUserIdByLoginToken(token); } } catch (error) { console.log('subscription context error: ', error) } // 从WebSocket握手请求中提取客户端IP let clientIp = ctx.extra.request.socket.remoteAddress; return { user: user, userId: userId, clientIp: clientIp, pubsub: mongodbpubsub }; } }, wsServer);
3. 确认PubSub实例一致性
确保服务端resolvers中使用的pubsub与上下文返回的是同一个mongodbpubsub实例,避免因实例不一致导致订阅无法接收消息。
4. 验证CORS配置
当前服务端的CORS配置已设置credentials: true和正确的origin,可覆盖WebSocket握手阶段的OPTIONS请求,无需额外修改。
完成以上修改后,重启服务与客户端,即可正常建立订阅连接并触发上下文函数。
内容的提问来源于stack exchange,提问作者VikR
相关产品推荐
相关产品推荐

