From cc50925c04ad02d60152cce44f09030ac57756d8 Mon Sep 17 00:00:00 2001 From: Joseph Nutt Date: Sat, 1 Nov 2025 13:07:31 +0000 Subject: [PATCH] fix(core): make get() throw for implicitly request-scoped trees Treat non-static dependency trees as scoped in get(); instruct consumers to use resolve().\n\nAdds focused tests under NestApplicationContext spec to cover implicit request scope via enhancers.\n\nCloses #15836. --- .../injector/abstract-instance-resolver.ts | 3 +- .../test/nest-application-context.spec.ts | 62 ++++++++++++++++++- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/packages/core/injector/abstract-instance-resolver.ts b/packages/core/injector/abstract-instance-resolver.ts index ed78674c778..44746c93ae0 100644 --- a/packages/core/injector/abstract-instance-resolver.ts +++ b/packages/core/injector/abstract-instance-resolver.ts @@ -29,7 +29,8 @@ export abstract class AbstractInstanceResolver { const pluckInstance = ({ wrapperRef }: InstanceLink) => { if ( wrapperRef.scope === Scope.REQUEST || - wrapperRef.scope === Scope.TRANSIENT + wrapperRef.scope === Scope.TRANSIENT || + !wrapperRef.isDependencyTreeStatic() ) { throw new InvalidClassScopeException(typeOrToken); } diff --git a/packages/core/test/nest-application-context.spec.ts b/packages/core/test/nest-application-context.spec.ts index 4fd31794a8e..13ce6e66069 100644 --- a/packages/core/test/nest-application-context.spec.ts +++ b/packages/core/test/nest-application-context.spec.ts @@ -1,4 +1,4 @@ -import { InjectionToken, Provider, Scope } from '@nestjs/common'; +import { InjectionToken, Provider, Scope, Injectable } from '@nestjs/common'; import { expect } from 'chai'; import * as sinon from 'sinon'; import { setTimeout } from 'timers/promises'; @@ -375,4 +375,64 @@ describe('NestApplicationContext', () => { }); }); }); + + describe('implicit request scope via enhancers', () => { + it('get() should throw when dependency tree is not static (request-scoped enhancer attached)', async () => { + class Host {} + @Injectable({ scope: Scope.REQUEST }) + class ReqScopedPipe {} + + const nestContainer = new NestContainer(); + const injector = new Injector(); + const instanceLoader = new InstanceLoader( + nestContainer, + injector, + new GraphInspector(nestContainer), + ); + const { moduleRef } = (await nestContainer.addModule(class T {}, []))!; + + // Register Host as a controller (matches real-world controller case) + nestContainer.addController(Host, moduleRef.token); + + // Register a request-scoped injectable and attach it as an enhancer to Host + // This simulates a method-level pipe/guard/interceptor making Host implicitly request-scoped + nestContainer.addInjectable(ReqScopedPipe, moduleRef.token, 'pipe', Host); + + const modules = nestContainer.getModules(); + await instanceLoader.createInstancesOfDependencies(modules); + + const appCtx = new NestApplicationContext(nestContainer); + + // With a non-static dependency tree, get() should refuse and instruct to use resolve() + expect(() => appCtx.get(Host)).to.throw(); + }); + + it('resolve() should instantiate when dependency tree is not static (request-scoped enhancer attached)', async () => { + class Host {} + @Injectable({ scope: Scope.REQUEST }) + class ReqScopedPipe {} + + const nestContainer = new NestContainer(); + const injector = new Injector(); + const instanceLoader = new InstanceLoader( + nestContainer, + injector, + new GraphInspector(nestContainer), + ); + const { moduleRef } = (await nestContainer.addModule(class T {}, []))!; + + // Register Host as a controller + nestContainer.addController(Host, moduleRef.token); + + nestContainer.addInjectable(ReqScopedPipe, moduleRef.token, 'pipe', Host); + + const modules = nestContainer.getModules(); + await instanceLoader.createInstancesOfDependencies(modules); + + const appCtx = new NestApplicationContext(nestContainer); + + const instance = await appCtx.resolve(Host); + expect(instance).instanceOf(Host); + }); + }); });