Which @angular/* package(s) are the source of the bug?
platform-server
Is this a regression?
No
Description
Problem
In SSR in renderApplication() (or analogically in renderModule()) when an error happens in the bootstrap() (or respectively in platformRef.bootstrapModule()) - for instance when one of the APP_INITIALIZER rejects - the PlatformRef is not destroyed. Then ngOnDestroy() hooks of singleton services are not called at the end of the SSR, which can lead to possible memory leaks in SSR.
Reason
The logic for destroying the PlatformRef is lying only at the end of the _redner() function body. So if an error happens earlier during the bootstrap() phase, the rest of the _render() function (including its ending which destroys the PlatformRef) is skipped.
Since the PlatformRef is not destroyed, the EnvironmentInjector (or the ModuleRef.injector) is not destroyed in cascade. Therefore the ngOnDestroy() hooks of singleton services provided in such an injector are not invoked. This can can lead to memory leaks in custom apps which have important teardown logics (e.g. unsubscribing from RxJs observables) in their singleton services' ngOnDestroy hooks).
Additional flaw in case of renderModule()
In other words, to fix the bug:
-
in case of renderApplication, it suffices to destroy PlatformRef even on failed bootstrap() function, e.g. wrap it with a try{} block and call the PlatformRef.destroy() in the finally{} block:
export async function renderApplication<T>(/*...*/) {
/* ... */
const platformRef = createServerPlatform(options);
try {
const applicationRef = await bootstrap();
return await _render(platformRef, applicationRef);
} finally {
platformRef.destroy();
}
-
in case of renderModule, we need to do the analogical fix as above (i.e. destroy PlatformRef in the try-finally block ) AND moreover I believe (but please correct me if I'm wrong!) we should setup the PLATFORM_DESTROY_LISTENER there, similarly to how we already do it for the renderApplication flow.
Please provide a link to a minimal reproduction of the bug
https://github.com/Platonn/ng-ssr-memory-leak-bug
Please provide the exception or error you saw
No response
Please provide the environment you discovered this bug in (run ng version)
Angular CLI: 18.2.5
Node: 20.14.0
Package Manager: npm 10.7.0
OS: darwin arm64
Angular: 18.2.5
... animations, cli, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, platform-server
... router, ssr
Package Version
@angular-devkit/architect 0.1802.5
@angular-devkit/build-angular 18.2.5
@angular-devkit/core 18.2.5
@angular-devkit/schematics 18.2.5
@schematics/angular 18.2.5
rxjs 7.8.1
typescript 5.5.4
zone.js 0.14.10
Anything else?
I've provided PR with a proposed fix #58112
Which @angular/* package(s) are the source of the bug?
platform-server
Is this a regression?
No
Description
Problem
In SSR in
renderApplication()(or analogically inrenderModule()) when an error happens in the bootstrap() (or respectively in platformRef.bootstrapModule()) - for instance when one of theAPP_INITIALIZERrejects - thePlatformRefis not destroyed. ThenngOnDestroy()hooks of singleton services are not called at the end of the SSR, which can lead to possible memory leaks in SSR.Reason
The logic for destroying the PlatformRef is lying only at the end of the
_redner()function body. So if an error happens earlier during thebootstrap()phase, the rest of the_render()function (including its ending which destroys thePlatformRef) is skipped.Since the
PlatformRefis not destroyed, theEnvironmentInjector(or theModuleRef.injector) is not destroyed in cascade. Therefore thengOnDestroy()hooks of singleton services provided in such an injector are not invoked. This can can lead to memory leaks in custom apps which have important teardown logics (e.g. unsubscribing from RxJs observables) in their singleton services'ngOnDestroyhooks).Additional flaw in case of
renderModule()renderApplication(), theEnvironmentInjectoris destroyed when thePlatformRefis destroyed thanks to setting up a PLATFORM_DESTROY_LISTENER.renderModule(), the the mainModuleRef(and its injector) is not destroyed, even if thePlatformRefis destroyed, because in the source code of Angular we currently we don't setup the PLATFORM_DESTROY_LISTENER in the case ofmoduleRender()flow. Moreover, we can't count on thePlatformRef.destroy()inner logic which invokesthis._modules.forEach(module=>module.destroy()), because thethis._modulesarray is empty until it's populated with the mainmoduleRefinside themoduleDoBootstrap()function. Unfortunately, if the error happens early enough, e.g. during resolving the the APP_INITIALIZERs phase, the error is thrown early and the control doesn't manage to reach the invocation ofmoduleDoBootstrap()in the end of thebootstrap()function body. Therefore thethis._modulesarray remain empty and the logicthis._modules.forEach(module=>module.destroy())does no help - the mainModuleRefis never destroyed.In other words, to fix the bug:
in case of
renderApplication, it suffices to destroyPlatformRefeven on failedbootstrap()function, e.g. wrap it with atry{}block and call thePlatformRef.destroy()in thefinally{}block:in case of
renderModule, we need to do the analogical fix as above (i.e. destroyPlatformRefin the try-finally block ) AND moreover I believe (but please correct me if I'm wrong!) we should setup thePLATFORM_DESTROY_LISTENERthere, similarly to how we already do it for therenderApplicationflow.Please provide a link to a minimal reproduction of the bug
https://github.com/Platonn/ng-ssr-memory-leak-bug
Please provide the exception or error you saw
No response
Please provide the environment you discovered this bug in (run
ng version)Angular CLI: 18.2.5
Node: 20.14.0
Package Manager: npm 10.7.0
OS: darwin arm64
Angular: 18.2.5
... animations, cli, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, platform-server
... router, ssr
Package Version
@angular-devkit/architect 0.1802.5
@angular-devkit/build-angular 18.2.5
@angular-devkit/core 18.2.5
@angular-devkit/schematics 18.2.5
@schematics/angular 18.2.5
rxjs 7.8.1
typescript 5.5.4
zone.js 0.14.10
Anything else?
I've provided PR with a proposed fix #58112