Skip to content

Commit c0d4086

Browse files
alan-agius4alxhub
authored andcommitted
fix(platform-server): surface errors during rendering (#50587)
Prior to this change in some cases errors tht happen during routing were not being surfaced. This is due to the fact that the router has floating promises, and the platform was being destroyed prior to these being settled. PR Close #50587
1 parent 4b16884 commit c0d4086

File tree

10 files changed

+127
-42
lines changed

10 files changed

+127
-42
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
/* tslint:disable:no-console */
9+
import 'zone.js/node';
10+
import {join} from 'path';
11+
import {renderModule} from '@angular/platform-server';
12+
import {readFileSync} from 'fs';
13+
import {AppServerModule} from './src/main.server';
14+
15+
const distFolder = join(process.cwd(), 'dist/ngmodule/browser');
16+
const indexHtml = readFileSync(join(distFolder, 'index.html'), 'utf-8');
17+
18+
async function runTest() {
19+
// Test and validate the errors are printed in the console.
20+
const originalConsoleError = console.error;
21+
const errors: string[] = [];
22+
console.error = (error, data) => errors.push(error.toString() + ' ' + data.toString());
23+
24+
try {
25+
await renderModule(AppServerModule, {
26+
document: indexHtml,
27+
url: '/error',
28+
});
29+
} catch {}
30+
31+
console.error = originalConsoleError;
32+
33+
// Test case
34+
if (!errors.some((e) => e.includes('Error in resolver'))) {
35+
errors.forEach(console.error);
36+
console.error(
37+
'\nError: expected rendering errors ("Error in resolver") to be printed in the console.\n'
38+
);
39+
process.exit(1);
40+
}
41+
}
42+
43+
runTest();

integration/platform-server/projects/ngmodule/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as express from 'express';
1313
import {AppServerModule} from './src/main.server';
1414
import {join} from 'path';
1515
import {readFileSync} from 'fs';
16+
import './prerender';
1617

1718
const app = express();
1819
const distFolder = join(process.cwd(), 'dist/ngmodule/browser');

integration/platform-server/projects/ngmodule/src/app/app-routing.module.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ const routes: Routes = [
2626
(m) => m.HttpTransferStateOnInitModule
2727
),
2828
},
29+
{
30+
path: 'error',
31+
component: HelloWorldComponent,
32+
resolve: {
33+
'id': () => {
34+
throw new Error('Error in resolver.');
35+
},
36+
},
37+
},
2938
];
3039

3140
@NgModule({
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
/* tslint:disable:no-console */
9+
import 'zone.js/node';
10+
import {renderApplication} from '@angular/platform-server';
11+
import bootstrap from './src/main.server';
12+
import {join} from 'path';
13+
import {readFileSync} from 'fs';
14+
15+
const distFolder = join(process.cwd(), 'dist/standalone/browser');
16+
const indexHtml = readFileSync(join(distFolder, 'index.html'), 'utf-8');
17+
18+
async function runTest() {
19+
// Test and validate the errors are printed in the console.
20+
const originalConsoleError = console.error;
21+
const errors: string[] = [];
22+
console.error = (error, data) => errors.push(error.toString() + ' ' + data.toString());
23+
24+
try {
25+
await renderApplication(bootstrap, {
26+
document: indexHtml,
27+
url: '/error',
28+
});
29+
} catch {}
30+
31+
console.error = originalConsoleError;
32+
33+
// Test case
34+
if (!errors.some((e) => e.includes('Error in resolver'))) {
35+
errors.forEach(console.error);
36+
console.error(
37+
'\nError: expected rendering errors ("Error in resolver") to be printed in the console.\n'
38+
);
39+
process.exit(1);
40+
}
41+
}
42+
43+
runTest();

integration/platform-server/projects/standalone/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as express from 'express';
1313
import bootstrap from './src/main.server';
1414
import {join} from 'path';
1515
import {readFileSync} from 'fs';
16+
import './prerender';
1617

1718
const app = express();
1819
const distFolder = join(process.cwd(), 'dist/standalone/browser');

integration/platform-server/projects/standalone/src/app/app-routing.module.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

integration/platform-server/projects/standalone/src/app/app.routes.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,13 @@ export const routes: Routes = [
2525
(c) => c.TransferStateOnInitComponent
2626
),
2727
},
28+
{
29+
path: 'error',
30+
component: HelloWorldComponent,
31+
resolve: {
32+
'id': () => {
33+
throw new Error('Error in resolver.');
34+
},
35+
},
36+
},
2837
];

packages/platform-server/src/utils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,15 @@ async function _render(platformRef: PlatformRef, applicationRef: ApplicationRef)
9090

9191
appendServerContextInfo(applicationRef);
9292
const output = platformState.renderToString();
93-
platformRef.destroy();
93+
94+
// Destroy the application in a macrotask, this allows pending promises to be settled and errors
95+
// to be surfaced to the users.
96+
await new Promise<void>((resolve) => {
97+
setTimeout(() => {
98+
platformRef.destroy();
99+
resolve();
100+
}, 0);
101+
});
94102

95103
return output;
96104
}

packages/platform-server/test/hydration_spec.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {CommonModule, DOCUMENT, isPlatformServer, NgComponentOutlet, NgFor, NgIf
1212
import {MockPlatformLocation} from '@angular/common/testing';
1313
import {ApplicationRef, Component, ComponentRef, createComponent, destroyPlatform, Directive, ElementRef, EnvironmentInjector, ErrorHandler, getPlatform, inject, Injectable, Input, NgZone, PLATFORM_ID, Provider, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ɵsetDocument} from '@angular/core';
1414
import {Console} from '@angular/core/src/console';
15-
import {InitialRenderPendingTasks} from '@angular/core/src/initial_render_pending_tasks';
1615
import {getComponentDef} from '@angular/core/src/render3/definition';
1716
import {NoopNgZone} from '@angular/core/src/zone/ng_zone';
1817
import {TestBed} from '@angular/core/testing';
@@ -213,7 +212,7 @@ function withDebugConsole() {
213212
return [{provide: Console, useClass: DebugConsole}];
214213
}
215214

216-
describe('platform-server integration', () => {
215+
describe('platform-server hydration integration', () => {
217216
beforeEach(() => {
218217
if (typeof ngDevMode === 'object') {
219218
// Reset all ngDevMode counters.
@@ -279,9 +278,6 @@ describe('platform-server integration', () => {
279278
async function hydrate(
280279
html: string, component: Type<unknown>, envProviders?: Provider[],
281280
hydrationFeatures: HydrationFeature<HydrationFeatureKind>[] = []): Promise<ApplicationRef> {
282-
// Destroy existing platform, a new one will be created later by the `bootstrapApplication`.
283-
destroyPlatform();
284-
285281
// Get HTML contents of the `<app>`, create a DOM element and append it into the body.
286282
const container = convertHtmlToDom(html, doc);
287283
Array.from(container.children).forEach(node => doc.body.appendChild(node));
@@ -4777,7 +4773,7 @@ describe('platform-server integration', () => {
47774773
}
47784774
}
47794775

4780-
ssr(SimpleComponent, undefined, withNoopErrorHandler()).catch((err: unknown) => {
4776+
await ssr(SimpleComponent, undefined, withNoopErrorHandler()).catch((err: unknown) => {
47814777
const message = (err as Error).message;
47824778
expect(message).toContain(
47834779
'During serialization, Angular was unable to find an element in the DOM');

packages/platform-server/test/integration_spec.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,11 @@ if (getDOM().supportsDOMEvents) return; // NODE only
532532

533533
describe('platform-server integration', () => {
534534
beforeEach(() => {
535-
if (getPlatform()) destroyPlatform();
535+
destroyPlatform();
536+
});
537+
538+
afterAll(() => {
539+
destroyPlatform();
536540
});
537541

538542
it('should bootstrap', async () => {
@@ -994,12 +998,11 @@ describe('platform-server integration', () => {
994998
renderApplication(
995999
getStandaloneBoostrapFn(MyServerAppStandalone, RenderHookProviders), options) :
9961000
renderModule(RenderHookModule, options);
997-
bootstrap.then(output => {
998-
// title should be added by the render hook.
999-
expect(output).toBe(
1000-
'<html><head><title>RenderHook</title></head><body>' +
1001-
'<app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">Works!</app></body></html>');
1002-
});
1001+
const output = await bootstrap;
1002+
// title should be added by the render hook.
1003+
expect(output).toBe(
1004+
'<html><head><title>RenderHook</title></head><body>' +
1005+
'<app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">Works!</app></body></html>');
10031006
});
10041007

10051008
it('should call multiple render hooks', async () => {

0 commit comments

Comments
 (0)