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会获取到空对象。
具体修复步骤
- 为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 } );
- 兼容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 } );
- 备选方案:启用自定义createSubscriptionServer
如果代码中注释的createSubscriptionServer已配置context,直接取消注释并使用该方法即可,确保自定义方法内部正确传递上下文。
验证修复
重启服务器后触发订阅请求,Resolver中的Context对象将能正确获取到setContext返回的内容。
内容的提问来源于stack exchange,提问作者Minhaj T
相关产品推荐
相关产品推荐

