Nest 单边 forwardRef 让请求作用域依赖挂起,12.2.0 补上循环检测
两个请求作用域provider形成循环,却仅一侧使用forwardRef,旧版解析可能永不完成。区分拆开循环、双边声明与12.2.0恢复错误检测的作用。
Nest 的 ModuleRef.resolve() 没有返回,也没有抛异常,请求一直挂着。如果两个 request-scoped provider 互相依赖,却只在一边写了 forwardRef(),要检查这个循环,而不是先延长接口超时。
在 Nest 11.2.3 与 12.1.3 的这组组合中,解析顺序会影响表现:一种顺序得到 CircularDependencyException,另一种顺序可能等不到完成。更早的 11.2.2 还可能返回带 undefined 依赖的半成品实例;回退到这个行为也不算正确完成。
单边 forwardRef 并没有让这个循环合法
例如 UpdateUsecase 使用 @Inject(forwardRef(() => CreateUsecase)),CreateUsecase 却直接注入 UpdateUsecase,两边都标为 Scope.REQUEST。这里至少有三项条件:循环构造依赖、请求作用域、仅一侧延迟引用。普通单例的启动错误,或非循环异步工厂一直不返回,不能按同一个问题处理。
受影响的注入器在加载一项尚无 donePromise 的 forward reference 时,没有给当前解析者的 SettlementSignal 登记这条依赖边。之后再遇到正在构建的对象,循环检测缺了这条边,便等待一个依赖自身才能完成的 Promise。多加一次 await 不会使它完成。
诊断时保留请求上下文与第一次解析的 provider。不要只反复交换调用顺序,让某次挂起变成报错就算解决;也不要靠定时器把未完成 Promise 留在后台,再把请求返回为成功。
先决定依赖图是否还需要这个环
如果可以调整职责,让两个 usecase 依赖一个不反向引用它们的公共服务,或由上层协调器按业务顺序调用,通常比保留构造循环更清楚。提取的服务也不能再注入原来的两个 usecase,否则只是把两节点的环改成三节点。
确需保留这类 provider 循环时,两侧都要按 Nest 的约定声明延迟引用。例如以下是 CreateUsecase 文件中的修改;UpdateUsecase 仍保留原有的对称注入,两个文件各自导入对方:
import { Inject, Injectable, Scope, forwardRef } from '@nestjs/common';
import { UpdateUsecase } from './update.usecase';
@Injectable({ scope: Scope.REQUEST })
export class CreateUsecase {
constructor(
@Inject(forwardRef(() => UpdateUsecase))
public readonly update: UpdateUsecase,
) {}
}
请求作用域的循环仍可能得到 undefined 依赖。应核对每个实际实例中的依赖;若仍有 undefined,优先拆开循环,不能因为两侧都加了注解,就认定任意请求作用域循环都可用。
provider 必须仍在真实模块的 providers、imports、exports 关系中可见。模块之间的循环有自己的 imports: [forwardRef(...)] 规则,不能用 provider 上的注解代替模块配置。构造器里也不要立即依赖对方已经完成初始化;延迟引用不保证构造先后,更不能把一个请求的数据缓存到共享单例里。
12.2.0 补上的是循环检测,不是自动接受单边配置
正式 Nest 12.2.0 的对应加载分支已在加载前调用 insertRef,发行内容也包含请求作用域 forward reference 的循环检测修正。采用兼容的 12.2.0 组合时,核对实际运行的 @nestjs/core,并按迁移要求同步相关 Nest 包;不能只更新全局 CLI。
这个修正使不成立的单边循环能够报错,并不承诺它现在可以正常注入。应用仍需拆开循环,或按其适用条件采用双边声明。对于需要留在 11.x 的部署,要核对那条维护线实际发行包是否含相同处理,不能从 12.2.0 的源码推定已回补。
验收要覆盖两个首次解析顺序,并让每次解析使用独立请求上下文。正确配置应完成解析、得到可用依赖;故意保留的错误配置应明确拒绝,不再永久等待。另查并行请求之间的数据隔离,避免为了绕开请求作用域把 provider 改成单例,反而串用了别的请求状态。


