You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 14:10:35