Node.js部署Heroku时MongoDB查询触发H13超时崩溃如何解决
问题概述
- 基于Express框架搭建的Node.js应用本地运行完全正常,接口可正常返回
hello world格式的JSON响应 - 部署到Heroku平台后,单次数据库查询操作就会导致应用运行约10秒后崩溃,Heroku返回H13错误,提示连接关闭且未返回响应
- 核心崩溃日志如下:
2022-06-20T04:01:55.992187+00:00 app[web.1]: const err = new MongooseError(message); 2022-06-20T04:01:55.992188+00:00 app[web.1]: ^ 2022-06-20T04:01:55.992189+00:00 app[web.1]: 2022-06-20T04:01:55.992190+00:00 app[web.1]: MongooseError: Operation `classifications.findOne()` buffering timed out after 10000ms 2022-06-20T04:01:55.992191+00:00 app[web.1]: at Timeout.<anonymous> (/app/node_modules/mongoose/lib/drivers/node-mongodb-native/collection.js:151:23) 2022-06-20T04:01:55.992191+00:00 app[web.1]: at listOnTimeout (node:internal/timers:559:17) 2022-06-20T04:01:55.992192+00:00 app[web.1]: at processTimers (node:internal/timers:502:7) 2022-06-20T04:01:55.992197+00:00 app[web.1]: Node.js v17.7.2 2022-06-20T04:01:56.002582+00:00 heroku[router]: at=error code=H13 desc="Connection closed without response" method=GET path="/al/classes/class-1a" host=wcsn-backend.herokuapp.com request_id=728b0fb8-d2bf-44f4-ae5c-793401a76653 fwd="216.198.98.78" dyno=web.1 connect=0ms service=10015ms status=503 bytes=0 protocol=https 2022-06-20T04:01:56.161890+00:00 heroku[web.1]: Process exited with status 1 2022-06-20T04:01:56.253251+00:00 heroku[web.1]: State changed from up to crashed
- 出问题的核心路由代码如下:
server.get('/:state/:classification/:className', async (req, res) => { let className = req.params.className.replaceAll('-', ' '); className = className.replaceAll('/', ' '); const { url } = await classificationsModel.findOne( { name: className.toLowerCase(), associatedState: req.params.state.toUpperCase(), }, { url: 1, _id: 0 } ); console.log(url); res.json('hello world'); teamList = []; });
问题根因
日志里的MongooseError: Operation classifications.findOne() buffering timed out after 10000ms是核心错误,和后续H13报错是明确的因果关系,没有玄学:
- Mongoose默认有缓冲机制:如果还没成功连上MongoDB,收到的所有查询操作都会先暂存在内存里,等连接成功后再正式执行,这个缓冲的默认超时时间刚好是10000ms
- 应用部署到Heroku后,从启动到崩溃从来没有成功连接过MongoDB,路由里的查询在缓冲区等满10秒后直接超时抛出未捕获异常,把Node.js进程直接打崩
- Heroku路由器发现处理请求的web dyno进程意外退出、没有返回任何HTTP响应,就会抛出H13(Connection closed without response)错误
- 本地能跑通纯粹是因为本地环境可以正常连上配置的Mongo,根本不会触发这个缓冲超时逻辑。
Heroku环境连不上Mongo是非常常见的部署问题,90%以上都是下面几个原因:
- 硬编码了本地数据库地址:本地开发时直接写死了
mongodb://localhost:27017/xxx这类本地连接串,Heroku的dyno是独立的云容器,里面没有运行Mongo服务,也访问不到个人电脑上开的数据库 - 云数据库IP白名单拦截:如果用的是MongoDB Atlas这类托管Mongo服务,默认只允许当前本地IP访问,没加Heroku的出口IP段的话,连接请求会被数据库直接拒绝
- 启动顺序错误:代码没等Mongoose连接完成就直接启动Express监听端口,请求进来的时候数据库连接还没建立;本地因为连本机数据库速度极快,这个时序问题根本复现不出来
- 连接串解析失败:Mongo连接串里的用户名、密码如果带
@、:、/这类特殊字符,必须做URL编码,不然本地可能解析正常,到Heroku读环境变量的时候就会解析错误,连不上库。
修复步骤
- 第一步:修正数据库配置
- 登录Heroku控制台,在应用的Config Vars配置项里添加
MONGO_URI,值填可公网访问的MongoDB实例连接串,不要在代码里硬编码本地数据库地址 - 如果使用MongoDB Atlas,先把IP白名单临时配置为
0.0.0.0/0(允许所有IP访问,排查完问题后可按需收紧权限),排除IP拦截问题 - 检查连接串里的特殊字符,用户名、密码段的特殊符号必须做URL编码,避免解析失败
- 登录Heroku控制台,在应用的Config Vars配置项里添加
- 第二步:调整服务启动逻辑
必须等Mongoose数据库连接成功后,再启动Express的端口监听,从根源避免请求进来时数据库还没连上的问题,参考实现:const mongoose = require('mongoose'); const express = require('express'); const server = express(); const PORT = process.env.PORT || 3000; async function bootstrap() { try { // 连接数据库,设置5秒连接超时方便快速排查问题 await mongoose.connect(process.env.MONGO_URI, { serverSelectionTimeoutMS: 5000 }); console.log('MongoDB connection established'); // 数据库连接成功后才启动HTTP服务 server.listen(PORT, () => { console.log(`Service listening on port ${PORT}`); }); } catch (err) { console.error('Database connection failed, exit process:', err); // 连不上数据库直接退出进程,不要接流量 process.exit(1); } } // 启动应用 bootstrap(); - 第三步:给路由加异常兜底
当前路由代码没有任何错误捕获,查询出错就会直接抛未捕获异常打崩进程,同时也没有处理查询无结果的边界情况,修改为如下实现:server.get('/:state/:classification/:className', async (req, res) => { try { let className = req.params.className.replaceAll('-', ' '); className = className.replaceAll('/', ' '); const classificationRes = await classificationsModel.findOne( { name: className.toLowerCase(), associatedState: req.params.state.toUpperCase(), }, { url: 1, _id: 0 } ); // 处理查询不到数据的情况 if (!classificationRes) { return res.status(404).json({ error: 'Resource not found' }); } console.log(classificationRes.url); return res.json('hello world'); } catch (err) { console.error('Query execution failed:', err); // 捕获错误后返回正常的错误响应,不要让进程崩溃 return res.status(500).json({ error: 'Internal service error' }); } }); - 第四步:部署验证
把修改后的代码推送到Heroku重新部署,通过heroku logs --tail命令查看实时日志:- 如果看到
MongoDB connection established日志,说明数据库连接正常,再请求接口即可正常响应 - 如果还是连接失败,日志会打印具体的连接错误原因(比如认证失败、IP被拦截、连接串格式错误),对应修正配置即可。
- 如果看到
内容的提问来源于stack exchange,提问作者Daniel Brannon
相关产品推荐
相关产品推荐

