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

GraphQL Subscription无法访问Resolver上下文,获取空对象求助

问题:GraphQL订阅Resolver无法获取Context对象(subscriptions-transport-ws)

问题描述

使用subscriptions-transport-ws库的SubscriptionServer实现GraphQL订阅服务器时,传递给订阅服务器的context对象在订阅Resolver中无法访问,获取到的是空对象。

相关代码及依赖

server.js

import mongoose from 'mongoose';
import express from 'express';
import helmet from 'helmet';
import favicon from 'serve-favicon';
import path from 'path';
import { fileURLToPath } from 'url';
import cors from 'cors';
import {
    ApolloServerPluginLandingPageGraphQLPlayground,
    ApolloServerPluginLandingPageDisabled,
    ApolloServerPluginDrainHttpServer
} from 'apollo-server-core';
import { ApolloServer } from 'apollo-server-express';
import { UserInputError } from 'apollo-server-errors';

import { ENVIRONMENT } from './config/environment.js';
import { environmentVariablesConfig } from './config/appConfig.js';
import { logger, endLogger } from './helpers/logger.js';
import { requestDevLogger } from './helpers/requestDevLogger.js';
import { setContext } from './gql/auth/setContext.js';
import { initTypeDefinition } from './gql/types/index.js';
import { resolvers } from './gql/resolvers/index.js';
import { getListOfIPV4Address } from './helpers/getListOfIPV4Address.js';
import routesManager from './routes/routesManager.js';

import { createServer } from 'http';
import { execute, subscribe } from 'graphql';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import createSubscriptionServer from './helpers/createSubscriptionServer.js';
import formatError from './helpers/formatError.js';

if (
    environmentVariablesConfig.formatConnection === 'DNSseedlist' &&
  environmentVariablesConfig.mongoDNSseedlist !== ''
) {
    mongoose.connect(environmentVariablesConfig.mongoDNSseedlist);
} else {
    if (environmentVariablesConfig.mongoUser !== '' && environmentVariablesConfig.mongoPass !== '') {
        mongoose.connect(
            `mongodb://${environmentVariablesConfig.mongoUser}:${environmentVariablesConfig.mongoPass}@${environmentVariablesConfig.dbHost}:${environmentVariablesConfig.dbPort}/${environmentVariablesConfig.database}`
        );
    } else {
        mongoose.connect(
            `mongodb://${environmentVariablesConfig.dbHost}:${environmentVariablesConfig.dbPort}/${environmentVariablesConfig.database}`
        );
    }
}

const db = mongoose.connection;
db.on('error', (err) => {
    logger.error(`Connection error with database. ${err}`);
});

db.once('open', () => {
    if (environmentVariablesConfig.environment !== ENVIRONMENT.DEVELOPMENT) {
        logger.info(`Connected with MongoDB service (${ENVIRONMENT.PRODUCTION} mode)`);
    } else {
        if (
            environmentVariablesConfig.formatConnection === 'DNSseedlist' &&
      environmentVariablesConfig.mongoDNSseedlist !== ''
        ) {
            logger.info(
                `Connected with MongoDB service at "${environmentVariablesConfig.mongoDNSseedlist}" using database "${environmentVariablesConfig.database}" (${ENVIRONMENT.DEVELOPMENT} mode)`
            );
        } else {
            logger.info(
                `Connected with MongoDB service at "${environmentVariablesConfig.dbHost}" in port "${environmentVariablesConfig.dbPort}" using database "${environmentVariablesConfig.database}" (${ENVIRONMENT.DEVELOPMENT} mode)`
            );
        }
    }

    initApplication();
});

const initApplication = async () => {
    const app = express();
    const httpServer = createServer(app);
    if (environmentVariablesConfig.environment === ENVIRONMENT.PRODUCTION) {
        app.use(helmet());
    } else {
        // Allow GraphQL Playground on development environments
        app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false }));
    }
    app.use(cors({ credentials: true }));

    const __dirname = path.dirname(fileURLToPath(import.meta.url));

    app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));

    app.use('', routesManager);

    const typeDefs = await initTypeDefinition();

    const schema = makeExecutableSchema({ typeDefs, resolvers });

    const server = new ApolloServer({
        schema,
        context: setContext,
        formatError,
        tracing: true,
        introspection: environmentVariablesConfig.environment === ENVIRONMENT.PRODUCTION ? false : true, // Set to "true" only in development mode
        plugins:
      environmentVariablesConfig.environment === ENVIRONMENT.PRODUCTION
        ? [ApolloServerPluginLandingPageDisabled()]
        : [requestDevLogger, ApolloServerPluginLandingPageGraphQLPlayground(), ApolloServerPluginDrainHttpServer({ httpServer })], // Log all querys and their responses. Show playground (do not use in production)
        // formatError (error) {
        //  if (!(error.originalError instanceof UserInputError)) {
        //      logger.error(error.message);
        //  }

        //  return error;
        // },
    });

    await server.start();

    server.applyMiddleware({ app });

    SubscriptionServer.create(
        { schema, execute, subscribe },
        { server: httpServer, path: server.graphqlPath }
    );

    // createSubscriptionServer({
    //  server: httpServer, // this attaches itself directly to the server
    //  schema,
    //  context: setContext,
    //  path: server.graphqlPath,
    // });

    app.use((req, res) => {
        res.status(404).send('404'); // eslint-disable-line no-magic-numbers
    });

    httpServer.listen(environmentVariablesConfig.port, () => {
        getListOfIPV4Address().forEach((ip) => {
            logger.info(`Application running on: http://${ip}:${environmentVariablesConfig.port}`);
            if (environmentVariablesConfig.environment !== ENVIRONMENT.PRODUCTION) {
                logger.info(
                    `GraphQL Playground running on: http://${ip}:${environmentVariablesConfig.port}${server.graphqlPath}`
                );
            }
        });
    });

    // Manage application shutdown
    process.on('SIGINT', () => {
        logger.info('Stopping application...');
        endLogger();
        process.exit();
    });
};

订阅Resolver

Subscription: {
        newUser: {
            subscribe: (_, __, Context) => {
                console.log('thisssss', Context);
                return pubsub.asyncIterator('NUMBER_INCREMENTED');
            }
        }
    }

GraphQL类型定义

import { gql } from 'apollo-server-express';

export default /* GraphQL */ gql`
  type User {
    email: String
    isAdmin: Boolean
    isActive: Boolean
    uuid: String
    registrationDate: String
    lastLogin: String
  }

  type Query {
    """
    Get list of all users registered on database
    """
    listAllUsers: [User]
  }
  type Subscription {
    newUser: String
  }
`;

依赖包及版本

"dependencies": {
    "@graphql-tools/load-files": "6.6.1",
    "@graphql-tools/merge": "8.3.14",
    "@graphql-tools/schema": "^9.0.14",
    "apollo-server-express": "3.11.1",
    "bcrypt": "5.1.0",
    "cors": "2.8.5",
    "dotenv": "16.0.3",
    "editorconfig": "^1.0.1",
    "express": "4.18.2",
    "graphql": "16.6.0",
    "graphql-subscriptions": "^2.0.0",
    "helmet": "6.0.1",
    "jsonwebtoken": "9.0.0",
    "lodash.merge": "4.6.2",
    "log4js": "6.7.1",
    "mongoose": "6.8.1",
    "serve-favicon": "2.5.0",
    "subscriptions-transport-ws": "^0.11.0"
  }

修复方案

核心问题

当前创建SubscriptionServer时未传入context配置项,Apollo Server的context仅作用于HTTP请求,WebSocket连接的上下文需要单独在SubscriptionServer中配置,否则Resolver会获取到空对象。

具体修复步骤

  1. 为SubscriptionServer添加context配置
    修改SubscriptionServer.create的参数,加入context函数,复用现有setContext逻辑并适配WebSocket的参数结构:
SubscriptionServer.create(
    { 
        schema, 
        execute, 
        subscribe,
        // 添加上下文配置
        context: async (ctx) => {
            // 从WebSocket连接的payload中提取授权头(根据客户端传参方式调整)
            const authHeader = ctx.connection.context.Authorization || ctx.connection.context.authorization;
            // 构造与HTTP请求一致的参数结构,传入setContext
            return setContext({ 
                req: { headers: { authorization: authHeader } } 
            });
        }
    },
    { server: httpServer, path: server.graphqlPath }
);
  1. 兼容WebSocket与HTTP的context逻辑(可选)
    如果需要让setContext直接支持两种场景,可以修改setContext函数:
// setContext.js示例
export const setContext = async ({ req, connection }) => {
    // 优先从WebSocket连接取授权信息
    const authHeader = connection?.context?.authorization || req?.headers?.authorization;
    const token = authHeader?.split(' ')[1];
    // 验证token并获取用户信息
    const user = await verifyToken(token);
    return { user };
};

此时SubscriptionServer的context可以直接传入setContext:

SubscriptionServer.create(
    { 
        schema, 
        execute, 
        subscribe,
        context: setContext
    },
    { server: httpServer, path: server.graphqlPath }
);
  1. 备选方案:启用自定义createSubscriptionServer
    如果代码中注释的createSubscriptionServer已配置context,直接取消注释并使用该方法即可,确保自定义方法内部正确传递上下文。

验证修复

重启服务器后触发订阅请求,Resolver中的Context对象将能正确获取到setContext返回的内容。

内容的提问来源于stack exchange,提问作者Minhaj T

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 09:20:22