Skip to content

Commit 02575d6

Browse files
arturovtalan-agius4
authored andcommitted
fix(@angular/ssr): handle platform destruction during rendering
Fixes crash when platform/app destroys itself during the bootstrapping and stabilization phase. Previously, the code would call `applicationRef.injector` without checking if the platform was destroyed, resulting in: "Error: Injector has already been destroyed" This can occur when: - Component constructor calls `inject(PlatformRef).destroy()` - AbortSignal triggers during request handling - APP_INITIALIZER rejects and causes cleanup - Custom guard/resolver logic destroys the platform Solution: Check `applicationRef.destroyed` after `whenStable()` and return error state instead of accessing destroyed injector. Test: Added test case that destroys app in component constructor to verify graceful handling of this edge case.
1 parent d9cd609 commit 02575d6

File tree

2 files changed

+28
-5
lines changed

2 files changed

+28
-5
lines changed

packages/angular/ssr/src/utils/ng.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ export async function renderAngular(
5959
url: URL,
6060
platformProviders: StaticProvider[],
6161
serverContext: string,
62-
): Promise<{ hasNavigationError: boolean; redirectTo?: string; content: () => Promise<string> }> {
62+
): Promise<
63+
| { hasNavigationError: true }
64+
| { hasNavigationError: boolean; redirectTo?: string; content: () => Promise<string> }
65+
> {
6366
// A request to `http://www.example.com/page/index.html` will render the Angular route corresponding to `http://www.example.com/page`.
6467
const urlToRender = stripIndexHtmlFromURL(url);
6568
const platformRef = platformServer([
@@ -100,6 +103,13 @@ export async function renderAngular(
100103
// Block until application is stable.
101104
await applicationRef.whenStable();
102105

106+
// This code protect against app destruction during bootstrapping which is a
107+
// valid case. We should not assume the `applicationRef` is not in destroyed state.
108+
// Calling `envInjector.get` would throw `NG0205: Injector has already been destroyed`.
109+
if (applicationRef.destroyed) {
110+
return { hasNavigationError: true };
111+
}
112+
103113
// TODO(alanagius): Find a way to avoid rendering here especially for redirects as any output will be discarded.
104114
const envInjector = applicationRef.injector;
105115
const routerIsProvided = !!envInjector.get(ActivatedRoute, null);

packages/angular/ssr/test/app_spec.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import '@angular/compiler';
1212
/* eslint-enable import/no-unassigned-import */
1313

1414
import { APP_BASE_HREF } from '@angular/common';
15-
import { Component, REQUEST, RESPONSE_INIT, inject } from '@angular/core';
16-
import { CanActivateFn, Router } from '@angular/router';
15+
import { Component, PlatformRef, REQUEST, RESPONSE_INIT, inject } from '@angular/core';
16+
import { ActivatedRoute, CanActivateFn, Router } from '@angular/router';
1717
import { AngularServerApp } from '../src/app';
1818
import { RenderMode } from '../src/routes/route-config';
1919
import { setAngularAppTestingManifest } from './testing-utils';
@@ -26,7 +26,13 @@ describe('AngularServerApp', () => {
2626
selector: 'app-home',
2727
template: `Home works`,
2828
})
29-
class HomeComponent {}
29+
class HomeComponent {
30+
constructor() {
31+
if (inject(ActivatedRoute).snapshot.data['destroyApp']) {
32+
inject(PlatformRef).destroy();
33+
}
34+
}
35+
}
3036

3137
@Component({
3238
selector: 'app-redirect',
@@ -65,7 +71,7 @@ describe('AngularServerApp', () => {
6571
{ path: 'home-ssg', component: HomeComponent },
6672
{ path: 'page-with-headers', component: HomeComponent },
6773
{ path: 'page-with-status', component: HomeComponent },
68-
74+
{ path: 'page-destroy-app', component: HomeComponent, data: { destroyApp: true } },
6975
{ path: 'redirect', redirectTo: 'home' },
7076
{ path: 'redirect-via-navigate', component: RedirectComponent },
7177
{
@@ -227,6 +233,13 @@ describe('AngularServerApp', () => {
227233
expect(response?.status).toBe(201);
228234
});
229235

236+
it('should not throw an error when app destroys itself', async () => {
237+
const response = await app.handle(new Request('http://localhost/page-destroy-app'));
238+
// The test expects response to be null, which is reasonable - if the app destroys
239+
// itself, there's nothing to render.
240+
expect(response).toBeNull();
241+
});
242+
230243
it('should return static `index.csr.html` for routes with CSR rendering mode', async () => {
231244
const response = await app.handle(new Request('http://localhost/home-csr'));
232245
const content = await response?.text();

0 commit comments

Comments
 (0)