Node.js+GraphQL Yoga+MongoDB部署Heroku启动超时问题求助
解决Heroku部署Node.js/GraphQL Yoga项目启动超时(H20错误)的问题
问题描述
我有一个基于Node.js、GraphQL Yoga和MongoDB的项目,本地构建与运行完全正常,但部署至Heroku平台后,尽管构建成功,项目启动时却立即崩溃。Heroku日志显示错误代码H20(应用启动超时),怀疑问题出在代码底部的App()函数或端口配置(本地为开发环境、Heroku为生产环境)。
项目代码(index.js)
const {execute, ExecutionArgs, subscribe} = require('graphql'); const {makeBehavior} = require('graphql-ws/lib/use/uWebSockets'); const {createSchema, createYoga, Repeater, createPubSub} = require('graphql-yoga'); const {App, HttpRequest, HttpResponse} = require('uWebSockets.js'); const typeDefs = require('./src/schema'); const resolvers = require('./src/resolvers/index'); const models = require('./src/models/index'); const {decode} = require('next-auth/jwt'); const {connect} = require('./src/db'); const {useResponseCache} = require("@graphql-yoga/plugin-response-cache"); const {useDeferStream} = require("@graphql-yoga/plugin-defer-stream"); // Connect to MongoDB connect(process.env.DB_HOST).then(() => { console.log("Connected to MongoDB!"); console.log(process.env.PORT, process.env.NODE_ENV) }).catch(e => { console.error(e); console.log('MongoDB connection error. Please make sure MongoDB is running.'); process.exit(); }); const pubSub = createPubSub() const yoga = createYoga({ cors: { origin: '*', methods: ['GET', 'POST'], allowedHeaders: ['Content-Type', 'Authorization'], }, schema: createSchema({ typeDefs: typeDefs, resolvers: resolvers, }), plugins: [ useDeferStream(), ], context: async ({ request: req}) => { let token = req?.headers?.get('authorization')?.split(' ')[1] || (req?.headers?.get("authorization")) // let token = req?.headers?.get('authorization')?.split(' ')[1]; //check if introspection query if (req?.body?.query?.includes("IntrospectionQuery")) { return { models, user: null, }; } const start = Date.now(); try { const jwtPayload = await decode({ token: token, secret: process.env.NEXTAUTH_SECRET, }); // console.log("jwtPayload", jwtPayload) const mid = new Date().getTime(); const user = await models.User.findOne({email: jwtPayload?.email}, { _id: 1, email: 1, preferences: 1, accountType: 1, }); const end = new Date().getTime(); const duration = end - start; // console.log(`Request took: ${duration}ms`, end - mid); return { pubSub, user, models, }; } catch (e) { console.log(e); return { models, user: null, }; } }, graphiql: { subscriptionsProtocol: 'WS', // use WebSockets instead of SSE }, }); // yoga's envelop may augment the `execute` and `subscribe` operations // so we need to make sure we always use the freshest instance const wsHandler = makeBehavior({ execute: args => (args).rootValue.execute(args), subscribe: args => (args).rootValue.subscribe(args), onSubscribe: async (ctx, msg) => { const {schema, execute, subscribe, contextFactory, parse, validate} = yoga.getEnveloped(ctx); const args = { schema, operationName: msg.payload.operationName, document: parse(msg.payload.query), variableValues: msg.payload.variables, contextValue: await contextFactory(), rootValue: { execute, subscribe, }, }; const errors = validate(args.schema, args.document); if (errors.length) return errors; return args; }, }); const port = process.env.NODE_ENV === 'production' ? process.env.PORT : 8080; console.log('PORT:', process.env.PORT); App() .any('/*', yoga) .ws(yoga.graphqlEndpoint, wsHandler) .listen(port, (token) => { if (token) { console.log(`Server is running on port ${port}`); } else { console.error('Failed to start server'); } }); // Handle graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM signal received. Shutting down gracefully.'); // Perform any necessary cleanup here process.exit(0); }); // export const app = App().any('/*', yoga).ws(yoga.graphqlEndpoint, wsHandler);
Heroku日志
2024-06-20T18:51:38.000000+00:00 app[api]: Build succeeded 2024-06-20T18:52:47.676067+00:00 heroku[router]: at=error code=H20 desc="App boot timeout" method=GET path="/" host=dev-mole-backend-239ec47a5642.herokuapp.com request_id=c27c796d-5109-401d-8041-9f1e8d3283b6 fwd="108.30.174.209" dyno= connect= service= status=503 bytes= protocol=https
问题分析与修复
核心问题
H20错误是Heroku检测到应用在60秒内未绑定到指定端口并响应请求。你的代码存在两个关键问题:
- MongoDB连接异步执行,服务器提前启动:服务器启动逻辑没有等待MongoDB连接完成,导致启动后数据库未就绪,后续请求阻塞超时。
- 端口配置逻辑冗余:Heroku始终会提供
PORT环境变量,无需区分生产/开发环境,多余的判断可能导致意外错误。
修复步骤
- 等待MongoDB连接完成后启动服务器:将服务器启动代码移至MongoDB连接的
then回调中,确保数据库就绪后再对外提供服务。 - 简化端口配置:直接使用
process.env.PORT || 8080,兼容本地和Heroku环境。 - 统一管理uWebSockets实例:避免重复创建App实例,减少潜在冲突。
修改后的代码
const {execute, ExecutionArgs, subscribe} = require('graphql'); const {makeBehavior} = require('graphql-ws/lib/use/uWebSockets'); const {createSchema, createYoga, Repeater, createPubSub} = require('graphql-yoga'); const {App, HttpRequest, HttpResponse} = require('uWebSockets.js'); const typeDefs = require('./src/schema'); const resolvers = require('./src/resolvers/index'); const models = require('./src/models/index'); const {decode} = require('next-auth/jwt'); const {connect} = require('./src/db'); const {useResponseCache} = require("@graphql-yoga/plugin-response-cache"); const {useDeferStream} = require("@graphql-yoga/plugin-defer-stream"); const pubSub = createPubSub() const yoga = createYoga({ cors: { origin: '*', methods: ['GET', 'POST'], allowedHeaders: ['Content-Type', 'Authorization'], }, schema: createSchema({ typeDefs: typeDefs, resolvers: resolvers, }), plugins: [ useDeferStream(), ], context: async ({ request: req}) => { let token = req?.headers?.get('authorization')?.split(' ')[1] || req?.headers?.get("authorization"); if (req?.body?.query?.includes("IntrospectionQuery")) { return { models, user: null, }; } try { const jwtPayload = await decode({ token: token, secret: process.env.NEXTAUTH_SECRET, }); const user = await models.User.findOne({email: jwtPayload?.email}, { _id: 1, email: 1, preferences: 1, accountType: 1, }); return { pubSub, user, models, }; } catch (e) { console.log(e); return { models, user: null, }; } }, graphiql: { subscriptionsProtocol: 'WS', }, }); const wsHandler = makeBehavior({ execute: args => args.rootValue.execute(args), subscribe: args => args.rootValue.subscribe(args), onSubscribe: async (ctx, msg) => { const {schema, execute, subscribe, contextFactory, parse, validate} = yoga.getEnveloped(ctx); const args = { schema, operationName: msg.payload.operationName, document: parse(msg.payload.query), variableValues: msg.payload.variables, contextValue: await contextFactory(), rootValue: { execute, subscribe, }, }; const errors = validate(args.schema, args.document); if (errors.length) return errors; return args; }, }); // 使用统一的端口配置 const port = process.env.PORT || 8080; console.log('PORT:', port); // 等待MongoDB连接完成后启动服务器 connect(process.env.DB_HOST).then(() => { console.log("Connected to MongoDB!"); console.log('NODE_ENV:', process.env.NODE_ENV); // 创建单一App实例 const app = App() .any('/*', yoga) .ws(yoga.graphqlEndpoint, wsHandler); app.listen(port, (token) => { if (token) { console.log(`Server is running on port ${port}`); } else { console.error('Failed to start server'); process.exit(1); } }); }).catch(e => { console.error('MongoDB connection failed:', e); process.exit(1); }); // 优雅关闭 process.on('SIGTERM', () => { console.log('SIGTERM received. Shutting down...'); process.exit(0); });
额外检查
- 确认Heroku环境变量(
DB_HOST、NEXTAUTH_SECRET等)已正确设置,可通过heroku config命令查看。 - 如果使用MongoDB Atlas,确保IP白名单已添加Heroku的动态IP(或临时允许所有IP用于测试)。
- 查看更详细的Heroku启动日志:
heroku logs --tail,实时监控启动过程中的错误信息。
内容的提问来源于stack exchange,提问作者Austin Edelman
相关产品推荐
相关产品推荐

