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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 11:09:53