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

NextJS集成GraphQL Apollo Subscription失败求助及问题排查

问题分析与解决方案

问题概述

使用NextJS开发基于GraphQL Subscription的聊天应用,后端采用graphql-yoga(NextJS Serverless环境),前端使用Apollo Client。后端在GraphQL Playground中运行正常,但前端集成后出现两个问题:

  1. 终端抛出错误:TypeError [ERR_INVALID_ARG_TYPE]: The "string" argument must be of type string or an instance of Buffer or ArrayBuffer. Received type function ([Function (anonymous)])
  2. 前端useSubscription返回{Error: undefined}

终端错误信息

error - TypeError [ERR_INVALID_ARG_TYPE]: The "string" argument must be of type string or an instance of Buffer or ArrayBuffer. Received type function ([Function (anonymous)])

代码片段

后端代码

import { createServer, createPubSub } from "@graphql-yoga/node";

const pubsub = createPubSub();

const NEW_MESSAGE = "NEW_MESSAGE";
const NEW_MESSAGES = "NEW_MESSAGES";
const messages = [];

const resolvers = {
  Query: {
    messages: (_, { name }, context) => messages,
  },
  Mutation: {
    createMessage(parent, { user, body }, { pubsub }) {
      const id = Math.floor(Math.random() * 10000000);
      messages.push({ id, user, body });

      pubsub.publish(NEW_MESSAGE, {
        newMessage: { id, user, body },
      });
      pubsub.publish(NEW_MESSAGES);

      return true;
    },
  },
  Subscription: {
    newMessage: {
      subscribe: (_, __, { pubsub }) => {
        return pubsub.subscribe(NEW_MESSAGE);
      },
      resolve: (payload) => {
        const { message } = payload;
        const { id, user, body } = message;
        return { id, user, body };
      },
    },
    newMessages: {
      subscribe: (_, __, { pubsub }) => {
        return pubsub.subscribe(NEW_MESSAGES);
      },
      resolve: () => messages,
    },
  },
};

const typeDefs = `
  type Message {
    id: ID!
    user: String!
    body: String!
  }

  type MessageInput {
    user: String!
    body: String!
  }

  type SubscriptionPayload {
    id: ID
    user: String
    body: String
  }

  type Query {
    messages: [Message!]
  }

  type Mutation {
    createMessage(user: String!, body: String!): Boolean!
  }

  type Subscription {
    newMessage: Message
    newMessages: [Message]
  }
`;

const server = createServer({
  context: async ({ req }) => {
    return {
      user: { name: "jogn Doe", id: 0 },
      pubsub,
    };
  },
  schema: {
    typeDefs,
    resolvers,
  },
});

export default server;

前端代码

_app.js

import "../styles/globals.css";
import { ApolloProvider } from "@apollo/client";
import { client } from "./apollo";

function MyApp({ Component, pageProps }) {
  return (
    <ApolloProvider client={client}>
      <Component {...pageProps} />
    </ApolloProvider>
  );
}

export default MyApp;

apollo.js

import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";

const uri = 'localhost:3000/api/graphql';

const httpLink = new HttpLink({
  uri: `http://${uri}`
});

const wsLink = () => {
  return new GraphQLWsLink(createClient({
    url: `ws://${uri}`
  }));
}

export const client = new ApolloClient({
  link: typeof window === 'undefined' ? httpLink : wsLink(),
  cache: new InMemoryCache(),
});

index.js

import Head from "next/head";
import styles from "../styles/Home.module.css";

import { gql, useSubscription } from "@apollo/client";
export default function Home() {

  const SUBSCRIPTION = gql`
    subscription newMessages {
      newMessages {
        id
        user
        body
      }
    }  
  `;

  const { loading, error, data } = useSubscription(SUBSCRIPTION);
  console.log({ data, loading, error });
  console.log(error); // {Error: undefined} is printed

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <div>Hello Nextjs Graphql</div>
      </main>
    </div>
  );
}

问题定位与修复

1. 前端Apollo Client链接配置错误

apollo.js中wsLink定义为函数,但Apollo Client的link参数需要直接传入Link实例,而非函数。同时建议用split自动区分HTTP和WebSocket操作:

修改后的apollo.js:

import { ApolloClient, HttpLink, InMemoryCache, split } from "@apollo/client";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
import { getMainDefinition } from '@apollo/client/utilities';

const uri = 'localhost:3000/api/graphql';

const httpLink = new HttpLink({
  uri: `http://${uri}`
});

const wsLink = new GraphQLWsLink(createClient({
  url: `ws://${uri}`
}));

// 自动根据操作类型选择链接
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink,
);

export const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});

2. 后端Subscription解析器与Publish逻辑错误

  • newMessage解析器中,payload的键是newMessage而非message,导致取值undefined:
    resolve: (payload) => {
      // 原代码错误:const { message } = payload;
      const { newMessage } = payload;
      const { id, user, body } = newMessage;
      return { id, user, body };
    },
    
  • newMessages发布时未传递payload,导致订阅无法获取数据:
    // 原代码错误:pubsub.publish(NEW_MESSAGES);
    pubsub.publish(NEW_MESSAGES, { newMessages: messages });
    
    对应的解析器可简化:
    resolve: (payload) => payload.newMessages,
    

3. NextJS Serverless环境适配问题

NextJS Serverless函数是无状态的,每次请求会启动新实例,默认内存PubSub无法跨实例共享数据,导致消息无法广播到所有在线用户。解决方案:

  • 使用Redis作为外部PubSub服务,替代内存PubSub
  • 将消息数据存储到Redis/数据库,而非内存数组

后端改造示例(Redis PubSub)

  1. 安装依赖:
npm install @graphql-yoga/plugin-redis-pubsub ioredis
  1. 修改后端代码:
import { createServer } from "@graphql-yoga/node";
import { useRedisPubSub } from '@graphql-yoga/plugin-redis-pubsub';
import Redis from 'ioredis';

// 初始化Redis客户端
const redis = new Redis({
  host: '你的Redis地址',
  port: 6379,
  // 若有密码则添加 password: '你的Redis密码'
});

const NEW_MESSAGE = "NEW_MESSAGE";
const NEW_MESSAGES = "NEW_MESSAGES";

// 改用Redis存储消息示例(替代内存数组)
const getMessages = async () => {
  const messageStrings = await redis.lrange('chat_messages', 0, -1);
  return messageStrings.map(JSON.parse);
};
const addMessage = async (message) => {
  await redis.lpush('chat_messages', JSON.stringify(message));
  // 限制消息数量,避免内存溢出
  await redis.ltrim('chat_messages', 0, 99);
};

const resolvers = {
  Query: {
    messages: async () => getMessages(),
  },
  Mutation: {
    createMessage: async (parent, { user, body }, { pubsub }) => {
      const id = Math.floor(Math.random() * 10000000);
      const message = { id, user, body };
      await addMessage(message);

      pubsub.publish(NEW_MESSAGE, { newMessage: message });
      pubsub.publish(NEW_MESSAGES, { newMessages: await getMessages() });

      return true;
    },
  },
  Subscription: {
    newMessage: {
      subscribe: (_, __, { pubsub }) => pubsub.subscribe(NEW_MESSAGE),
      resolve: (payload) => payload.newMessage,
    },
    newMessages: {
      subscribe: (_, __, { pubsub }) => pubsub.subscribe(NEW_MESSAGES),
      resolve: (payload) => payload.newMessages,
    },
  },
};

const server = createServer({
  plugins: [
    useRedisPubSub({
      redis,
      publish: { channelPrefix: 'chat-app:' },
      subscribe: { channelPrefix: 'chat-app:' }
    })
  ],
  context: async ({ req }) => {
    return {
      user: { name: "jogn Doe", id: 0 },
      // PubSub由插件自动注入,无需手动传入
    };
  },
  schema: {
    typeDefs,
    resolvers,
  },
});

export default server;

总结

  1. 前端优先修复Apollo Client的链接配置,确保WebSocket链接实例正确传入
  2. 后端修正Subscription解析器的payload取值与Publish逻辑
  3. NextJS Serverless环境下必须使用外部PubSub服务(如Redis)实现跨实例消息广播,同时消息数据需存储到外部介质,不能依赖内存

内容的提问来源于stack exchange,提问作者Gwenda Thomas

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 13:50:34