Skip to content

Commit cc50925

Browse files
committed
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.
1 parent ba16478 commit cc50925

File tree

2 files changed

+63
-2
lines changed

2 files changed

+63
-2
lines changed

packages/core/injector/abstract-instance-resolver.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ export abstract class AbstractInstanceResolver {
2929
const pluckInstance = ({ wrapperRef }: InstanceLink) => {
3030
if (
3131
wrapperRef.scope === Scope.REQUEST ||
32-
wrapperRef.scope === Scope.TRANSIENT
32+
wrapperRef.scope === Scope.TRANSIENT ||
33+
!wrapperRef.isDependencyTreeStatic()
3334
) {
3435
throw new InvalidClassScopeException(typeOrToken);
3536
}

packages/core/test/nest-application-context.spec.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { InjectionToken, Provider, Scope } from '@nestjs/common';
1+
import { InjectionToken, Provider, Scope, Injectable } from '@nestjs/common';
22
import { expect } from 'chai';
33
import * as sinon from 'sinon';
44
import { setTimeout } from 'timers/promises';
@@ -375,4 +375,64 @@ describe('NestApplicationContext', () => {
375375
});
376376
});
377377
});
378+
379+
describe('implicit request scope via enhancers', () => {
380+
it('get() should throw when dependency tree is not static (request-scoped enhancer attached)', async () => {
381+
class Host {}
382+
@Injectable({ scope: Scope.REQUEST })
383+
class ReqScopedPipe {}
384+
385+
const nestContainer = new NestContainer();
386+
const injector = new Injector();
387+
const instanceLoader = new InstanceLoader(
388+
nestContainer,
389+
injector,
390+
new GraphInspector(nestContainer),
391+
);
392+
const { moduleRef } = (await nestContainer.addModule(class T {}, []))!;
393+
394+
// Register Host as a controller (matches real-world controller case)
395+
nestContainer.addController(Host, moduleRef.token);
396+
397+
// Register a request-scoped injectable and attach it as an enhancer to Host
398+
// This simulates a method-level pipe/guard/interceptor making Host implicitly request-scoped
399+
nestContainer.addInjectable(ReqScopedPipe, moduleRef.token, 'pipe', Host);
400+
401+
const modules = nestContainer.getModules();
402+
await instanceLoader.createInstancesOfDependencies(modules);
403+
404+
const appCtx = new NestApplicationContext(nestContainer);
405+
406+
// With a non-static dependency tree, get() should refuse and instruct to use resolve()
407+
expect(() => appCtx.get(Host)).to.throw();
408+
});
409+
410+
it('resolve() should instantiate when dependency tree is not static (request-scoped enhancer attached)', async () => {
411+
class Host {}
412+
@Injectable({ scope: Scope.REQUEST })
413+
class ReqScopedPipe {}
414+
415+
const nestContainer = new NestContainer();
416+
const injector = new Injector();
417+
const instanceLoader = new InstanceLoader(
418+
nestContainer,
419+
injector,
420+
new GraphInspector(nestContainer),
421+
);
422+
const { moduleRef } = (await nestContainer.addModule(class T {}, []))!;
423+
424+
// Register Host as a controller
425+
nestContainer.addController(Host, moduleRef.token);
426+
427+
nestContainer.addInjectable(ReqScopedPipe, moduleRef.token, 'pipe', Host);
428+
429+
const modules = nestContainer.getModules();
430+
await instanceLoader.createInstancesOfDependencies(modules);
431+
432+
const appCtx = new NestApplicationContext(nestContainer);
433+
434+
const instance = await appCtx.resolve(Host);
435+
expect(instance).instanceOf(Host);
436+
});
437+
});
378438
});

0 commit comments

Comments
 (0)