Description
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 case of
renderApplication()
, theEnvironmentInjector
is destroyed when thePlatformRef
is destroyed thanks to setting up a PLATFORM_DESTROY_LISTENER. - but in case of
renderModule()
, the the mainModuleRef
(and its injector) is not destroyed, even if thePlatformRef
is 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._modules
array is empty until it's populated with the mainmoduleRef
inside 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._modules
array remain empty and the logicthis._modules.forEach(module=>module.destroy())
does no help - the mainModuleRef
is never destroyed.
In other words, to fix the bug:
-
in case of
renderApplication
, it suffices to destroyPlatformRef
even on failedbootstrap()
function, e.g. wrap it with atry{}
block and call thePlatformRef.destroy()
in thefinally{}
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. destroyPlatformRef
in the try-finally block ) AND moreover I believe (but please correct me if I'm wrong!) we should setup thePLATFORM_DESTROY_LISTENER
there, similarly to how we already do it for therenderApplication
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