Skip to content

Commit 1b84662

Browse files
committed
fix(platform-server): destroy PlatformRef even if the bootstrap or _render fail
1 parent a7fcbde commit 1b84662

2 files changed

Lines changed: 170 additions & 11 deletions

File tree

packages/platform-server/src/utils.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -194,18 +194,20 @@ async function _render(platformRef: PlatformRef, applicationRef: ApplicationRef)
194194
}
195195

196196
appendServerContextInfo(applicationRef);
197-
const output = platformState.renderToString();
197+
return platformState.renderToString();
198+
}
198199

199-
// Destroy the application in a macrotask, this allows pending promises to be settled and errors
200-
// to be surfaced to the users.
200+
/**
201+
* Destroy the application in a macrotask, this allows pending promises to be settled and errors
202+
* to be surfaced to the users.
203+
*/
204+
async function asyncDestroyPlatform(platformRef: PlatformRef): Promise<void> {
201205
await new Promise<void>((resolve) => {
202206
setTimeout(() => {
203207
platformRef.destroy();
204208
resolve();
205209
}, 0);
206210
});
207-
208-
return output;
209211
}
210212

211213
/**
@@ -248,9 +250,13 @@ export async function renderModule<T>(
248250
): Promise<string> {
249251
const {document, url, extraProviders: platformProviders} = options;
250252
const platformRef = createServerPlatform({document, url, platformProviders});
251-
const moduleRef = await platformRef.bootstrapModule(moduleType);
252-
const applicationRef = moduleRef.injector.get(ApplicationRef);
253-
return _render(platformRef, applicationRef);
253+
try {
254+
const moduleRef = await platformRef.bootstrapModule(moduleType);
255+
const applicationRef = moduleRef.injector.get(ApplicationRef);
256+
return await _render(platformRef, applicationRef);
257+
} finally {
258+
await asyncDestroyPlatform(platformRef);
259+
}
254260
}
255261

256262
/**
@@ -279,8 +285,11 @@ export async function renderApplication<T>(
279285
): Promise<string> {
280286
return runAndMeasurePerf('renderApplication', async () => {
281287
const platformRef = createServerPlatform(options);
282-
283-
const applicationRef = await bootstrap();
284-
return _render(platformRef, applicationRef);
288+
try {
289+
const applicationRef = await bootstrap();
290+
return await _render(platformRef, applicationRef);
291+
} finally {
292+
await asyncDestroyPlatform(platformRef);
293+
}
285294
});
286295
}

packages/platform-server/test/integration_spec.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ import {
4242
ViewEncapsulation,
4343
ɵPendingTasks as PendingTasks,
4444
ɵwhenStable as whenStable,
45+
APP_INITIALIZER,
46+
inject,
47+
getPlatform,
4548
} from '@angular/core';
4649
import {SSR_CONTENT_INTEGRITY_MARKER} from '@angular/core/src/hydration/utils';
4750
import {TestBed} from '@angular/core/testing';
@@ -1076,6 +1079,153 @@ class HiddenModule {}
10761079
);
10771080
},
10781081
);
1082+
1083+
it(
1084+
`should call onOnDestroy of a service after a successful render` +
1085+
`(standalone: ${isStandalone})`,
1086+
async () => {
1087+
let wasServiceNgOnDestroyCalled = false;
1088+
1089+
@Injectable({providedIn: 'root'})
1090+
class DestroyableService {
1091+
ngOnDestroy() {
1092+
wasServiceNgOnDestroyCalled = true;
1093+
}
1094+
}
1095+
1096+
const SuccessfulAppInitializerProviders = [
1097+
{
1098+
provide: APP_INITIALIZER,
1099+
useFactory: () => {
1100+
inject(DestroyableService);
1101+
return () => Promise.resolve(); // Success in APP_INITIALIZER
1102+
},
1103+
multi: true,
1104+
},
1105+
];
1106+
1107+
@NgModule({
1108+
providers: SuccessfulAppInitializerProviders,
1109+
imports: [MyServerAppModule, ServerModule],
1110+
bootstrap: [MyServerApp],
1111+
})
1112+
class ServerSuccessfulAppInitializerModule {}
1113+
1114+
const ServerSuccessfulAppInitializerAppStandalone = getStandaloneBootstrapFn(
1115+
createMyServerApp(true),
1116+
SuccessfulAppInitializerProviders,
1117+
);
1118+
1119+
const options = {document: doc};
1120+
const bootstrap = isStandalone
1121+
? renderApplication(ServerSuccessfulAppInitializerAppStandalone, options)
1122+
: renderModule(ServerSuccessfulAppInitializerModule, options);
1123+
await bootstrap;
1124+
1125+
expect(getPlatform()).withContext('PlatformRef should be destroyed').toBeNull();
1126+
expect(wasServiceNgOnDestroyCalled)
1127+
.withContext('DestroyableService.ngOnDestroy() should be called')
1128+
.toBeTrue();
1129+
},
1130+
);
1131+
1132+
it(
1133+
`should call onOnDestroy of a service after some APP_INITIALIZER fails ` +
1134+
`(standalone: ${isStandalone})`,
1135+
async () => {
1136+
let wasServiceNgOnDestroyCalled = false;
1137+
1138+
@Injectable({providedIn: 'root'})
1139+
class DestroyableService {
1140+
ngOnDestroy() {
1141+
wasServiceNgOnDestroyCalled = true;
1142+
}
1143+
}
1144+
1145+
const FailingAppInitializerProviders = [
1146+
{
1147+
provide: APP_INITIALIZER,
1148+
useFactory: () => {
1149+
inject(DestroyableService);
1150+
return () => Promise.reject('Error in APP_INITIALIZER');
1151+
},
1152+
multi: true,
1153+
},
1154+
];
1155+
1156+
@NgModule({
1157+
providers: FailingAppInitializerProviders,
1158+
imports: [MyServerAppModule, ServerModule],
1159+
bootstrap: [MyServerApp],
1160+
})
1161+
class ServerFailingAppInitializerModule {}
1162+
1163+
const ServerFailingAppInitializerAppStandalone = getStandaloneBootstrapFn(
1164+
createMyServerApp(true),
1165+
FailingAppInitializerProviders,
1166+
);
1167+
debugger;
1168+
const options = {document: doc};
1169+
const bootstrap = isStandalone
1170+
? renderApplication(ServerFailingAppInitializerAppStandalone, options)
1171+
: renderModule(ServerFailingAppInitializerModule, options);
1172+
await expectAsync(bootstrap).toBeRejectedWith('Error in APP_INITIALIZER');
1173+
1174+
expect(getPlatform()).withContext('PlatformRef should be destroyed').toBeNull();
1175+
expect(wasServiceNgOnDestroyCalled)
1176+
.withContext('DestroyableService.ngOnDestroy() should be called')
1177+
.toBeTrue();
1178+
},
1179+
);
1180+
1181+
it(
1182+
`should call onOnDestroy of a service after an error happens in a root component's constructor ` +
1183+
`(standalone: ${isStandalone})`,
1184+
async () => {
1185+
let wasServiceNgOnDestroyCalled = false;
1186+
1187+
@Injectable({providedIn: 'root'})
1188+
class DestroyableService {
1189+
ngOnDestroy() {
1190+
wasServiceNgOnDestroyCalled = true;
1191+
}
1192+
}
1193+
1194+
@Component({
1195+
standalone: isStandalone,
1196+
selector: 'app',
1197+
template: `Works!`,
1198+
})
1199+
class MyServerFailingConstructorApp {
1200+
constructor() {
1201+
inject(DestroyableService);
1202+
throw 'Error in constructor of the root component';
1203+
}
1204+
}
1205+
1206+
@NgModule({
1207+
declarations: [MyServerFailingConstructorApp],
1208+
imports: [MyServerAppModule, ServerModule],
1209+
bootstrap: [MyServerFailingConstructorApp],
1210+
})
1211+
class MyServerFailingConstructorAppModule {}
1212+
1213+
const MyServerFailingConstructorAppStandalone = getStandaloneBootstrapFn(
1214+
MyServerFailingConstructorApp,
1215+
);
1216+
const options = {document: doc};
1217+
const bootstrap = isStandalone
1218+
? renderApplication(MyServerFailingConstructorAppStandalone, options)
1219+
: renderModule(MyServerFailingConstructorAppModule, options);
1220+
await expectAsync(bootstrap).toBeRejectedWith(
1221+
'Error in constructor of the root component',
1222+
);
1223+
expect(getPlatform()).withContext('PlatformRef should be destroyed').toBeNull();
1224+
expect(wasServiceNgOnDestroyCalled)
1225+
.withContext('DestroyableService.ngOnDestroy() should be called')
1226+
.toBeTrue();
1227+
},
1228+
);
10791229
});
10801230
});
10811231

0 commit comments

Comments
 (0)