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

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秒内未绑定到指定端口并响应请求。你的代码存在两个关键问题:

  1. MongoDB连接异步执行,服务器提前启动:服务器启动逻辑没有等待MongoDB连接完成,导致启动后数据库未就绪,后续请求阻塞超时。
  2. 端口配置逻辑冗余:Heroku始终会提供PORT环境变量,无需区分生产/开发环境,多余的判断可能导致意外错误。

修复步骤

  1. 等待MongoDB连接完成后启动服务器:将服务器启动代码移至MongoDB连接的then回调中,确保数据库就绪后再对外提供服务。
  2. 简化端口配置:直接使用process.env.PORT || 8080,兼容本地和Heroku环境。
  3. 统一管理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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 03:44:57