DEBUG.WIKI
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →
OnlineSIM:在线接收短信,虚拟号码租用
雨云;给项目一台云服务器

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 的正式钩子实现仍可见相关递归结构,不能据此声称某个新版本已经消除这类悬挂;还需要按具体注册图验证。若减少不必要实例后仍卡住,继续按钩子边界排查未完成的异步操作,而不是无限增加启动等待时间。