diff --git a/packages/angular/ssr/src/utils/ng.ts b/packages/angular/ssr/src/utils/ng.ts index 16e059e6aaf2..acf2df180f32 100644 --- a/packages/angular/ssr/src/utils/ng.ts +++ b/packages/angular/ssr/src/utils/ng.ts @@ -59,7 +59,10 @@ export async function renderAngular( url: URL, platformProviders: StaticProvider[], serverContext: string, -): Promise<{ hasNavigationError: boolean; redirectTo?: string; content: () => Promise }> { +): Promise< + | { hasNavigationError: true } + | { hasNavigationError: boolean; redirectTo?: string; content: () => Promise } +> { // A request to `http://www.example.com/page/index.html` will render the Angular route corresponding to `http://www.example.com/page`. const urlToRender = stripIndexHtmlFromURL(url); const platformRef = platformServer([ @@ -100,6 +103,13 @@ export async function renderAngular( // Block until application is stable. await applicationRef.whenStable(); + // This code protect against app destruction during bootstrapping which is a + // valid case. We should not assume the `applicationRef` is not in destroyed state. + // Calling `envInjector.get` would throw `NG0205: Injector has already been destroyed`. + if (applicationRef.destroyed) { + return { hasNavigationError: true }; + } + // TODO(alanagius): Find a way to avoid rendering here especially for redirects as any output will be discarded. const envInjector = applicationRef.injector; const routerIsProvided = !!envInjector.get(ActivatedRoute, null); diff --git a/packages/angular/ssr/test/app_spec.ts b/packages/angular/ssr/test/app_spec.ts index 9584feafeb72..a72c4d75fae2 100644 --- a/packages/angular/ssr/test/app_spec.ts +++ b/packages/angular/ssr/test/app_spec.ts @@ -12,8 +12,8 @@ import '@angular/compiler'; /* eslint-enable import/no-unassigned-import */ import { APP_BASE_HREF } from '@angular/common'; -import { Component, REQUEST, RESPONSE_INIT, inject } from '@angular/core'; -import { CanActivateFn, Router } from '@angular/router'; +import { Component, PlatformRef, REQUEST, RESPONSE_INIT, inject } from '@angular/core'; +import { ActivatedRoute, CanActivateFn, Router } from '@angular/router'; import { AngularServerApp } from '../src/app'; import { RenderMode } from '../src/routes/route-config'; import { setAngularAppTestingManifest } from './testing-utils'; @@ -26,7 +26,13 @@ describe('AngularServerApp', () => { selector: 'app-home', template: `Home works`, }) - class HomeComponent {} + class HomeComponent { + constructor() { + if (inject(ActivatedRoute).snapshot.data['destroyApp']) { + inject(PlatformRef).destroy(); + } + } + } @Component({ selector: 'app-redirect', @@ -65,7 +71,7 @@ describe('AngularServerApp', () => { { path: 'home-ssg', component: HomeComponent }, { path: 'page-with-headers', component: HomeComponent }, { path: 'page-with-status', component: HomeComponent }, - + { path: 'page-destroy-app', component: HomeComponent, data: { destroyApp: true } }, { path: 'redirect', redirectTo: 'home' }, { path: 'redirect-via-navigate', component: RedirectComponent }, { @@ -227,6 +233,13 @@ describe('AngularServerApp', () => { expect(response?.status).toBe(201); }); + it('should not throw an error when app destroys itself', async () => { + const response = await app.handle(new Request('http://localhost/page-destroy-app')); + // The test expects response to be null, which is reasonable - if the app destroys + // itself, there's nothing to render. + expect(response).toBeNull(); + }); + it('should return static `index.csr.html` for routes with CSR rendering mode', async () => { const response = await app.handle(new Request('http://localhost/home-csr')); const content = await response?.text();