mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
fix(core): fix race condition in class dependency resolution
Fix race condition in class dependency resolution, which could otherwise lead to undefined or null injection. This is a followup to #15405 that fixes another case of this bug that was not taken care of in the previous PR. In this case specifically, the staticity of the dependency tree could be checked before all dependencies are loaded and the param / property Barrier is passed, resulting in a potentially wrong evaluation of the `isTreeStatic` property of the `InstanceWrapper`. Closes #4873
This commit is contained in:
@@ -49,21 +49,42 @@ class TransientProvider {}
|
||||
@Injectable()
|
||||
class RequestProvider {}
|
||||
|
||||
@Injectable()
|
||||
class ForeignTransientProvider {}
|
||||
|
||||
@Injectable()
|
||||
export class Dependant {
|
||||
constructor(
|
||||
private readonly transientProvider: TransientProvider,
|
||||
|
||||
private readonly foreignTransientProvider: ForeignTransientProvider,
|
||||
|
||||
@Inject(RequestProvider)
|
||||
private readonly requestProvider: RequestProvider,
|
||||
) {}
|
||||
|
||||
public checkDependencies() {
|
||||
expect(this.transientProvider).to.be.instanceOf(TransientProvider);
|
||||
expect(this.foreignTransientProvider).to.be.instanceOf(
|
||||
ForeignTransientProvider,
|
||||
);
|
||||
expect(this.requestProvider).to.be.instanceOf(RequestProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: ForeignTransientProvider,
|
||||
scope: Scope.TRANSIENT,
|
||||
useClass: ForeignTransientProvider,
|
||||
},
|
||||
],
|
||||
exports: [ForeignTransientProvider],
|
||||
})
|
||||
export class ModuleWithForeignTransientProvider {}
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
@@ -98,6 +119,12 @@ export class GlobalModuleWithRequestProvider {}
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
/*
|
||||
* ForeginTransientProvider will be resolved quickly because its host module is imported first.
|
||||
* IMPORTANT: Do not move this module, otherwise we may not catch future regressions.
|
||||
*/
|
||||
ModuleWithForeignTransientProvider,
|
||||
|
||||
GlobalModule1,
|
||||
GlobalModule2,
|
||||
GlobalModule3,
|
||||
@@ -115,7 +142,7 @@ export class GlobalModuleWithRequestProvider {}
|
||||
export class AppModule {}
|
||||
|
||||
describe('Many global modules', () => {
|
||||
it('should inject request-scoped useFactory provider and transient-scoped useClass provider from different modules', async () => {
|
||||
it('should inject request-scoped and transient-scoped providers from different modules', async () => {
|
||||
const moduleBuilder = Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
});
|
||||
|
||||
@@ -2263,6 +2263,22 @@
|
||||
},
|
||||
"id": "-1303681274"
|
||||
},
|
||||
"-831049991": {
|
||||
"source": "594986539",
|
||||
"target": "-1721730431",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "InputModule",
|
||||
"sourceClassName": "InputService",
|
||||
"targetClassName": "CircularService",
|
||||
"sourceClassToken": "InputService",
|
||||
"targetClassToken": "CircularService",
|
||||
"targetModuleName": "CircularModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-831049991"
|
||||
},
|
||||
"-886102564": {
|
||||
"source": "208171089",
|
||||
"target": "671882984",
|
||||
@@ -2280,6 +2296,22 @@
|
||||
},
|
||||
"id": "-886102564"
|
||||
},
|
||||
"-2146943494": {
|
||||
"source": "-234035039",
|
||||
"target": "928565345",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "RequestChainModule",
|
||||
"sourceClassName": "RequestChainService",
|
||||
"targetClassName": "HelperService",
|
||||
"sourceClassToken": "RequestChainService",
|
||||
"targetClassToken": "HelperService",
|
||||
"targetModuleName": "HelperModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-2146943494"
|
||||
},
|
||||
"-2003045613": {
|
||||
"source": "-377928898",
|
||||
"target": "-616397055",
|
||||
@@ -2312,38 +2344,6 @@
|
||||
},
|
||||
"id": "-881420795"
|
||||
},
|
||||
"-831049991": {
|
||||
"source": "594986539",
|
||||
"target": "-1721730431",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "InputModule",
|
||||
"sourceClassName": "InputService",
|
||||
"targetClassName": "CircularService",
|
||||
"sourceClassToken": "InputService",
|
||||
"targetClassToken": "CircularService",
|
||||
"targetModuleName": "CircularModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-831049991"
|
||||
},
|
||||
"-2146943494": {
|
||||
"source": "-234035039",
|
||||
"target": "928565345",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "RequestChainModule",
|
||||
"sourceClassName": "RequestChainService",
|
||||
"targetClassName": "HelperService",
|
||||
"sourceClassToken": "RequestChainService",
|
||||
"targetClassToken": "HelperService",
|
||||
"targetModuleName": "HelperModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-2146943494"
|
||||
},
|
||||
"-1816180282": {
|
||||
"source": "-848516688",
|
||||
"target": "-1673986099",
|
||||
|
||||
@@ -2181,22 +2181,6 @@
|
||||
},
|
||||
"id": "1976848738"
|
||||
},
|
||||
"-2105726668": {
|
||||
"source": "-1803759743",
|
||||
"target": "1010833816",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "PropertiesModule",
|
||||
"sourceClassName": "PropertiesService",
|
||||
"targetClassName": "token",
|
||||
"sourceClassToken": "PropertiesService",
|
||||
"targetClassToken": "token",
|
||||
"targetModuleName": "PropertiesModule",
|
||||
"keyOrIndex": "token",
|
||||
"injectionType": "property"
|
||||
},
|
||||
"id": "-2105726668"
|
||||
},
|
||||
"-21463590": {
|
||||
"source": "-1378706112",
|
||||
"target": "1004276345",
|
||||
@@ -2213,6 +2197,22 @@
|
||||
},
|
||||
"id": "-21463590"
|
||||
},
|
||||
"-2105726668": {
|
||||
"source": "-1803759743",
|
||||
"target": "1010833816",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "PropertiesModule",
|
||||
"sourceClassName": "PropertiesService",
|
||||
"targetClassName": "token",
|
||||
"sourceClassToken": "PropertiesService",
|
||||
"targetClassToken": "token",
|
||||
"targetModuleName": "PropertiesModule",
|
||||
"keyOrIndex": "token",
|
||||
"injectionType": "property"
|
||||
},
|
||||
"id": "-2105726668"
|
||||
},
|
||||
"-1657371464": {
|
||||
"source": "-1673986099",
|
||||
"target": "1919157847",
|
||||
@@ -2247,6 +2247,22 @@
|
||||
},
|
||||
"id": "-1303681274"
|
||||
},
|
||||
"-831049991": {
|
||||
"source": "594986539",
|
||||
"target": "-1721730431",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "InputModule",
|
||||
"sourceClassName": "InputService",
|
||||
"targetClassName": "CircularService",
|
||||
"sourceClassToken": "InputService",
|
||||
"targetClassToken": "CircularService",
|
||||
"targetModuleName": "CircularModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-831049991"
|
||||
},
|
||||
"-886102564": {
|
||||
"source": "208171089",
|
||||
"target": "671882984",
|
||||
@@ -2264,6 +2280,22 @@
|
||||
},
|
||||
"id": "-886102564"
|
||||
},
|
||||
"-2146943494": {
|
||||
"source": "-234035039",
|
||||
"target": "928565345",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "RequestChainModule",
|
||||
"sourceClassName": "RequestChainService",
|
||||
"targetClassName": "HelperService",
|
||||
"sourceClassToken": "RequestChainService",
|
||||
"targetClassToken": "HelperService",
|
||||
"targetModuleName": "HelperModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-2146943494"
|
||||
},
|
||||
"-2003045613": {
|
||||
"source": "-377928898",
|
||||
"target": "-616397055",
|
||||
@@ -2296,38 +2328,6 @@
|
||||
},
|
||||
"id": "-881420795"
|
||||
},
|
||||
"-831049991": {
|
||||
"source": "594986539",
|
||||
"target": "-1721730431",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "InputModule",
|
||||
"sourceClassName": "InputService",
|
||||
"targetClassName": "CircularService",
|
||||
"sourceClassToken": "InputService",
|
||||
"targetClassToken": "CircularService",
|
||||
"targetModuleName": "CircularModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-831049991"
|
||||
},
|
||||
"-2146943494": {
|
||||
"source": "-234035039",
|
||||
"target": "928565345",
|
||||
"metadata": {
|
||||
"type": "class-to-class",
|
||||
"sourceModuleName": "RequestChainModule",
|
||||
"sourceClassName": "RequestChainService",
|
||||
"targetClassName": "HelperService",
|
||||
"sourceClassToken": "RequestChainService",
|
||||
"targetClassToken": "HelperService",
|
||||
"targetModuleName": "HelperModule",
|
||||
"keyOrIndex": 0,
|
||||
"injectionType": "constructor"
|
||||
},
|
||||
"id": "-2146943494"
|
||||
},
|
||||
"-1816180282": {
|
||||
"source": "-848516688",
|
||||
"target": "-1673986099",
|
||||
|
||||
@@ -680,14 +680,11 @@ export class Injector {
|
||||
inquirerId,
|
||||
);
|
||||
if (!instanceHost.isResolved && !instanceWrapperRef.forwardRef) {
|
||||
wrapper.settlementSignal?.insertRef(instanceWrapperRef.id);
|
||||
|
||||
await this.loadProvider(
|
||||
instanceWrapperRef,
|
||||
relatedModule,
|
||||
contextId,
|
||||
wrapper,
|
||||
);
|
||||
/*
|
||||
* Provider will be loaded shortly in resolveComponentHost() once we pass the current
|
||||
* Barrier. We cannot load it here because doing so could incorrectly evaluate the
|
||||
* staticity of the dependency tree and lead to undefined / null injection.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,63 +447,6 @@ describe('Injector', () => {
|
||||
),
|
||||
).to.eventually.be.eq(null);
|
||||
});
|
||||
|
||||
it('should call "loadProvider" when component is not resolved', async () => {
|
||||
const moduleFixture = {
|
||||
imports: new Map([
|
||||
[
|
||||
'key',
|
||||
{
|
||||
providers: {
|
||||
has: () => true,
|
||||
get: () =>
|
||||
new InstanceWrapper({
|
||||
isResolved: false,
|
||||
}),
|
||||
},
|
||||
exports: {
|
||||
has: () => true,
|
||||
},
|
||||
imports: new Map(),
|
||||
},
|
||||
],
|
||||
] as any),
|
||||
};
|
||||
await injector.lookupComponentInImports(
|
||||
moduleFixture as any,
|
||||
metatype as any,
|
||||
new InstanceWrapper(),
|
||||
);
|
||||
expect(loadProvider.called).to.be.true;
|
||||
});
|
||||
|
||||
it('should not call "loadProvider" when component is resolved', async () => {
|
||||
const moduleFixture = {
|
||||
relatedModules: new Map([
|
||||
[
|
||||
'key',
|
||||
{
|
||||
providers: {
|
||||
has: () => true,
|
||||
get: () => ({
|
||||
isResolved: true,
|
||||
}),
|
||||
},
|
||||
exports: {
|
||||
has: () => true,
|
||||
},
|
||||
relatedModules: new Map(),
|
||||
},
|
||||
],
|
||||
] as any),
|
||||
};
|
||||
await injector.lookupComponentInImports(
|
||||
moduleFixture as any,
|
||||
metatype as any,
|
||||
null!,
|
||||
);
|
||||
expect(loadProvider.called).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveParamToken', () => {
|
||||
|
||||
Reference in New Issue
Block a user