Fastify 大量独立插件让 app.ready() 一直不完成
每条路由建立封装实例,再由 Swagger 添加 onReady 钩子,可能触发与栈容量有关的启动悬挂。按真实隔离边界整合路由,保留启动失败与权限验收。
再增加一组路由,Fastify 就停在 await app.ready(),既不完成也没有正常抛出启动错误。若应用为每条路由单独 register() 一个插件,还启用了 Swagger,先统计插件实例和 onReady 钩子,不能只盯着路由总数。
Fastify 5.10.0、@fastify/swagger 9.8.0、Node.js 24.15.0 的具体组合中,大量这种注册会触发与调用栈容量有关的启动悬挂。四百或八百不是 Fastify 的通用路由上限;插件数量、钩子数量、Node 参数和执行位置都会改变临界点。
路由数量相同,实例与钩子数量可以很不同
普通 register() 会建立封装上下文。Swagger 9.8.0 的动态模式在子实例注册时,还会为其添加收集 schema 的 onReady 钩子。每条路由包一层插件,会把简单的路由集合变成许多实例及启动回调。
Fastify 的应用钩子遍历与子实例处理包含递归调用。调用栈耗尽时可能看到 Maximum call stack size exceeded,也可能只留下不再完成的 ready;不能把“没输出栈错误”理解为没有启动异常。具体由哪个队列环节吞掉或未传播错误,还应按实际栈和依赖实现核对,不把它一概归成 Swagger 解析 schema 过慢。
先在隔离的启动入口比较两件事:保留同样路由定义,减少无必要的插件封装后能否完成;保留实例结构,只暂时停用文档插件后行为是否变化。后者只用于定位,不能把关闭文档功能当作业务启动已经完整修复。
按真正的隔离边界组织路由
如果这些路由本来共享装饰器、鉴权和 schema,可以评估放进一个有限范围的插件,而不是每条都建立独立实例。下面是已有 app 和 reportRoutes 路由定义数组的结构示例:
app.register(async function reportModule(scope) {
for (const route of reportRoutes) {
scope.route(route);
}
});
reportRoutes 中每项仍需提供原来的 method、url、handler 等定义;文档插件按原要求在路由之前注册。迁移时保存原有配置,比较路由前缀、鉴权钩子、schema 可见性与装饰器,不能只追求实例更少。
不同租户或模块原本依赖封装隔离时,不应直接合并。也不要给所有插件套上跳过封装的标记:那会改变可见范围,可能把一处启动故障变成权限或模块边界错误。
超时应让启动失败,不应放行流量
部署入口应有明确的启动截止时间和失败收尾。ready 未完成时不要报告健康或接收业务流量;仅用 Promise.race() 返回一个超时字符串,并不会取消原来的启动任务,也不会自动释放已经打开的连接。
增加 Node 的栈容量可能改变现象,但它依赖运行环境,不能写成固定参数保证所有路由数都安全。延长插件超时也不会修复已经卡住的队列。
完成调整后,在目标 Node 和实际依赖组合中检查 ready、监听、代表性路由、鉴权以及文档中的 schema 是否都正常。Fastify 5.12.7 的正式钩子实现仍可见相关递归结构,不能据此声称某个新版本已经消除这类悬挂;还需要按具体注册图验证。若减少不必要实例后仍卡住,继续按钩子边界排查未完成的异步操作,而不是无限增加启动等待时间。


