Apollo Server v4 WebSocket连接失败问题求助
WebSocket连接失败问题排查与修复
问题描述
WebSocket连接到ws://localhost:4000/graphql失败,报错信息:
WebSocket connection to 'ws://localhost:4000/graphql' failed:
SubscriptionClient.connect @ client.ts:557
(anonymous) @ client.ts:522
环境:电商项目,Apollo Server从v2/v3升级到v4,使用MySQL数据库、AWS S3存储图片。
服务端代码
import express from "express"; import { ApolloServer } from "@apollo/server"; import { expressMiddleware } from "@apollo/server/express4"; import { createServer } from "http"; import dotenv from "dotenv"; import { getAuthUser } from "./utils.js"; import typeDefs from "./schema.js"; import resolvers from "./resolvers/index.js"; import cors from "cors"; import { fileURLToPath } from "url"; import path from "path"; import { graphqlUploadExpress } from "graphql-upload"; import pubsub from "./pubsub.cjs"; dotenv.config(); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); app.use(cors()); app.use(express.json({ limit: "50mb" })); app.use(express.urlencoded({ limit: "50mb", extended: true })); app.use( "/graphql", graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }) ); const apolloServer = new ApolloServer({ typeDefs, resolvers, playground: true, context: async ({ req }) => { const user = await getAuthUser(req); const { operationName, variables } = req.body; console.log("Operation:", operationName); console.log("Variables:", variables); console.log("User object:", user); if (user) { console.log("User role:", user.role); user.isAdmin = user.role === "admin"; } console.log("User object after isAdmin assignment:", user); return { user, req, pubsub }; }, uploads: false, subscriptions: { onConnect: async (connectionParams, webSocket) => { console.log("Websocket connection established."); if (connectionParams.authToken) { const fakeRequest = { headers: { authorization: `Bearer ${connectionParams.authToken}` }, }; const user = await getAuthUser(fakeRequest); console.log(user); if (!user) { console.log("Invalid auth token!"); return; } return { user, pubsub }; } console.log("Missing auth token!"); return; }, }, }); await apolloServer.start(); const apolloExpressMiddleware = expressMiddleware(apolloServer); app.use("/graphql", apolloExpressMiddleware); const httpServer = createServer(app); httpServer.listen({ port: process.env.PORT || 4000 }, () => { console.log( `Server ready at http://localhost:${process.env.PORT || 4000}${ apolloServer.graphqlPath }` ); console.log( `Subscriptions ready at ws://localhost:${process.env.PORT || 4000}${ apolloServer.subscriptionsPath }` ); console.log("Server has started."); });
客户端代码
import { ApolloClient, InMemoryCache, HttpLink, ApolloLink, split, gql, } from "@apollo/client"; import { setContext } from "@apollo/client/link/context"; import { onError } from "@apollo/client/link/error"; import { createUploadLink } from "apollo-upload-client"; import { getMainDefinition } from "@apollo/client/utilities"; import { WebSocketLink } from "@apollo/client/link/ws"; const httpLink = new HttpLink({ uri: "http://localhost:4000/graphql" }); const uploadLink = createUploadLink({ uri: "http://localhost:4000/graphql" }); const authLink = setContext((request, previousContext) => { const token = localStorage.getItem("authToken"); return { ...previousContext, headers: { ...previousContext.headers, authorization: token ? `Bearer ${token}` : "", }, }; }); const errorLink = onError(({ graphQLErrors, networkError, operation }) => { console.log("Operation:", operation.operationName); console.log("Variables:", operation.variables); if (graphQLErrors) { graphQLErrors.map(({ message, locations, path, extensions }) => console.log( `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}, Extensions: ${JSON.stringify( extensions )}` ) ); } if (networkError) { console.log(`[Network error]: ${networkError}`); } }); const wsLink = new WebSocketLink({ uri: `ws://localhost:4000/graphql`, options: { reconnect: true, connectionParams: () => { const token = localStorage.getItem("authToken"); console.log("Auth Token:", token); return { authToken: token, }; }, connectionCallback: (error) => { console.log("WebSocket Connection Callback"); if (error) { console.error("Error from connectionCallback:", error); if (error.message === "Invalid auth token!") { localStorage.removeItem("authToken"); } } }, }, }); const isFileUploadOperation = ({ variables }) => { return typeof variables.file !== "undefined"; }; const isSubscriptionOperation = ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === "OperationDefinition" && definition.operation === "subscription" ); }; const link = ApolloLink.from([ errorLink, split( isSubscriptionOperation, wsLink, split( isFileUploadOperation, authLink.concat(uploadLink), authLink.concat(httpLink) ) ), ]); const client = new ApolloClient({ link: errorLink.concat(link), cache: new InMemoryCache(), }); export default client;
核心问题分析与修复步骤
1. Apollo Server v4订阅配置变更
Apollo Server v4不再内置WebSocket订阅支持,需单独安装依赖并通过插件配置:
服务端修改:
- 安装依赖:
npm install @apollo/server graphql-ws ws @graphql-tools/schema
- 替换原有订阅配置,改为使用
graphql-ws:
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { WebSocketServer } from 'ws'; import { useServer } from 'graphql-ws/lib/use/ws'; // ... 原有代码保留 ... // 创建可执行Schema const schema = makeExecutableSchema({ typeDefs, resolvers }); const apolloServer = new ApolloServer({ schema, playground: true, context: async ({ req }) => { // 原有context逻辑不变 const user = await getAuthUser(req); const { operationName, variables } = req.body; console.log("Operation:", operationName); console.log("Variables:", variables); console.log("User object:", user); if (user) { console.log("User role:", user.role); user.isAdmin = user.role === "admin"; } console.log("User object after isAdmin assignment:", user); return { user, req, pubsub }; }, uploads: false, plugins: [ ApolloServerPluginDrainHttpServer({ httpServer }), { async serverWillStart() { return { async drainServer() { await serverCleanup.dispose(); }, }; }, }, ], }); await apolloServer.start(); const apolloExpressMiddleware = expressMiddleware(apolloServer); app.use("/graphql", apolloExpressMiddleware); // 配置WebSocket服务器 const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql', // 与GraphQL HTTP端点保持同路径 }); const serverCleanup = useServer({ schema, context: async (ctx) => { // 原onConnect逻辑迁移到这里 const { connectionParams } = ctx; console.log("Websocket connection established."); if (connectionParams.authToken) { const fakeRequest = { headers: { authorization: `Bearer ${connectionParams.authToken}` }, }; const user = await getAuthUser(fakeRequest); console.log(user); if (!user) { console.log("Invalid auth token!"); return { user: null, pubsub }; } return { user, pubsub }; } console.log("Missing auth token!"); return { user: null, pubsub }; }, }, wsServer); httpServer.listen({ port: process.env.PORT || 4000 }, () => { console.log( `Server ready at http://localhost:${process.env.PORT || 4000}/graphql` ); console.log( `Subscriptions ready at ws://localhost:${process.env.PORT || 4000}/graphql` ); console.log("Server has started."); });
2. 客户端WebSocketLink适配
Apollo Client原WebSocketLink依赖旧版订阅协议,需替换为graphql-ws的链接:
客户端修改:
- 安装依赖:
npm install graphql-ws
- 替换原
wsLink定义:
import { createClient } from 'graphql-ws'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; // 替换原wsLink const wsLink = new GraphQLWsLink(createClient({ url: 'ws://localhost:4000/graphql', connectionParams: () => { const token = localStorage.getItem("authToken"); console.log("Auth Token:", token); return { authToken: token, }; }, on: { error: (error) => { console.error("WebSocket error:", error); if (error.message === "Invalid auth token!") { localStorage.removeItem("authToken"); } }, }, }));
3. 其他潜在问题排查
- CORS配置优化:确保允许WebSocket跨域请求:
app.use(cors({ origin: "http://localhost:3000", // 替换为你的前端地址 credentials: true, }));
- 路径与端口校验:确认服务端WebSocket路径和客户端完全一致,均为
/graphql - Token传递验证:通过客户端控制台日志确认
connectionParams是否正确获取到localStorage中的token
验证修复
启动服务后,检查服务端日志是否输出Subscriptions ready at ws://localhost:4000/graphql;客户端发起订阅时,服务端是否打印Websocket connection established.且无报错。
内容的提问来源于stack exchange,提问作者Mang
相关产品推荐
相关产品推荐

