From abfb04a3421023d7121dbda34ce8c7406a3d8e11 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:03:20 +0000 Subject: [PATCH] refactor(platform-server): replace standard Error with RuntimeError (#69184) Update platform-server to use Angular 's native `RuntimeError` class. This aligns error throwing patterns in platform-server with other packages of the framework such as core, common, and platform-browser. For URL and host errors, the error messages are configured to return only the raw dynamic URL when `ngDevMode` is false (in production) to aid in troubleshooting without bloating production bundles. PR Close #69184 --- .../public-api/platform-server/index.api.md | 18 +++++++++++++++ .../platform-server/src/domino_adapter.ts | 8 ++++++- packages/platform-server/src/errors.ts | 21 +++++++++++++++++ packages/platform-server/src/http.ts | 10 ++++++-- .../platform-server/src/platform-server.ts | 1 + .../platform-server/src/platform_state.ts | 8 ++++++- packages/platform-server/src/url.ts | 23 +++++++++++++++---- packages/platform-server/src/utils.ts | 9 +++++++- .../platform-server/test/integration_spec.ts | 6 ++--- .../test/platform_location_spec.ts | 4 ++-- packages/platform-server/test/url_spec.ts | 4 ++-- 11 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 packages/platform-server/src/errors.ts diff --git a/goldens/public-api/platform-server/index.api.md b/goldens/public-api/platform-server/index.api.md index 7dcee0513f2..984923c3042 100644 --- a/goldens/public-api/platform-server/index.api.md +++ b/goldens/public-api/platform-server/index.api.md @@ -63,6 +63,24 @@ export function renderModule(moduleType: Type, options: { allowedHosts?: Readonly[]; }): Promise; +// @public +export const enum RuntimeErrorCode { + // (undocumented) + DISABLED_DOM_EMULATION_IN_NON_BROWSER = 5704, + // (undocumented) + GET_COOKIE_NOT_IMPLEMENTED = 5700, + // (undocumented) + HOST_NOT_ALLOWED = 5706, + // (undocumented) + INVALID_URL = 5701, + // (undocumented) + PROTOCOL_RELATIVE_URL_NOT_ALLOWED = 5702, + // (undocumented) + SUSPICIOUS_URL_CHANGE_ORIGIN = 5703, + // (undocumented) + XHR_NOT_LOADED = 5705 +} + // @public export class ServerModule { // (undocumented) diff --git a/packages/platform-server/src/domino_adapter.ts b/packages/platform-server/src/domino_adapter.ts index e4f1b70b195..bf19662f411 100644 --- a/packages/platform-server/src/domino_adapter.ts +++ b/packages/platform-server/src/domino_adapter.ts @@ -7,8 +7,11 @@ */ import {ɵsetRootDomAdapter as setRootDomAdapter} from '@angular/common'; +import {ɵRuntimeError as RuntimeError} from '@angular/core'; import {ɵBrowserDomAdapter as BrowserDomAdapter} from '@angular/platform-browser'; +import {RuntimeErrorCode} from './errors'; + // g3-only import domino from 'domino'; import domino from '../third_party/domino/bundled-domino'; // 3p-only @@ -116,6 +119,9 @@ export class DominoAdapter extends BrowserDomAdapter { } override getCookie(name: string): string { - throw new Error('getCookie has not been implemented'); + throw new RuntimeError( + RuntimeErrorCode.GET_COOKIE_NOT_IMPLEMENTED, + (typeof ngDevMode === 'undefined' || ngDevMode) && 'getCookie has not been implemented', + ); } } diff --git a/packages/platform-server/src/errors.ts b/packages/platform-server/src/errors.ts new file mode 100644 index 00000000000..68bb0d768f3 --- /dev/null +++ b/packages/platform-server/src/errors.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +/** + * The list of error codes used in runtime code of the `platform-server` package. + * Reserved error code range: 5700-5800. + */ +export const enum RuntimeErrorCode { + GET_COOKIE_NOT_IMPLEMENTED = 5700, + INVALID_URL = 5701, + PROTOCOL_RELATIVE_URL_NOT_ALLOWED = 5702, + SUSPICIOUS_URL_CHANGE_ORIGIN = 5703, + DISABLED_DOM_EMULATION_IN_NON_BROWSER = 5704, + XHR_NOT_LOADED = 5705, + HOST_NOT_ALLOWED = 5706, +} diff --git a/packages/platform-server/src/http.ts b/packages/platform-server/src/http.ts index 837a985db5c..509aba309ce 100644 --- a/packages/platform-server/src/http.ts +++ b/packages/platform-server/src/http.ts @@ -13,8 +13,10 @@ import { HttpHandlerFn, HttpRequest, } from '@angular/common/http'; -import {inject, Injectable, Provider} from '@angular/core'; +import {inject, Injectable, Provider, ɵRuntimeError as RuntimeError} from '@angular/core'; import {Observable} from 'rxjs'; + +import {RuntimeErrorCode} from './errors'; import {resolveUrl} from './url'; @Injectable() @@ -35,7 +37,11 @@ export class ServerXhr implements XhrFactory { build(): XMLHttpRequest { const impl = this.xhrImpl; if (!impl) { - throw new Error('Unexpected state in ServerXhr: XHR implementation is not loaded.'); + throw new RuntimeError( + RuntimeErrorCode.XHR_NOT_LOADED, + (typeof ngDevMode === 'undefined' || ngDevMode) && + 'Unexpected state in ServerXhr: XHR implementation is not loaded.', + ); } return new impl.XMLHttpRequest(); diff --git a/packages/platform-server/src/platform-server.ts b/packages/platform-server/src/platform-server.ts index 04f283a8199..f17d2f627bb 100644 --- a/packages/platform-server/src/platform-server.ts +++ b/packages/platform-server/src/platform-server.ts @@ -11,6 +11,7 @@ export {provideServerRendering} from './provide_server'; export {platformServer, ServerModule} from './server'; export {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformConfig} from './tokens'; export {renderApplication, renderModule} from './utils'; +export {RuntimeErrorCode} from './errors'; export * from './private_export'; export {VERSION} from './version'; diff --git a/packages/platform-server/src/platform_state.ts b/packages/platform-server/src/platform_state.ts index c066fcebbcf..6849fc7d82e 100644 --- a/packages/platform-server/src/platform_state.ts +++ b/packages/platform-server/src/platform_state.ts @@ -14,8 +14,10 @@ import { Injector, ɵstartMeasuring as startMeasuring, ɵstopMeasuring as stopMeasuring, + ɵRuntimeError as RuntimeError, } from '@angular/core'; +import {RuntimeErrorCode} from './errors'; import {serializeDocument} from './domino_adapter'; import {ENABLE_DOM_EMULATION} from './tokens'; @@ -36,7 +38,11 @@ export class PlatformState { */ renderToString(): string { if (ngDevMode && !this._enableDomEmulation && !window?.document) { - throw new Error('Disabled DOM emulation should only run in browser environments'); + throw new RuntimeError( + RuntimeErrorCode.DISABLED_DOM_EMULATION_IN_NON_BROWSER, + (typeof ngDevMode === 'undefined' || ngDevMode) && + 'Disabled DOM emulation should only run in browser environments', + ); } const measuringLabel = 'renderToString'; diff --git a/packages/platform-server/src/url.ts b/packages/platform-server/src/url.ts index 3d4418506ff..e8a7c00f97d 100644 --- a/packages/platform-server/src/url.ts +++ b/packages/platform-server/src/url.ts @@ -6,6 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ +import {ɵRuntimeError as RuntimeError} from '@angular/core'; + +import {RuntimeErrorCode} from './errors'; + /** * Matches http: or https: */ @@ -69,7 +73,10 @@ export function resolveUrl( // absolute URL. Since it is malformed, the native URL constructor will throw a validation // error. Standard relative/protocol-relative paths parse successfully, allowing the flow to continue. if (!URL.canParse(urlStr, 'http://fake')) { - throw new Error(`Invalid URL: ${urlStr}`); + throw new RuntimeError( + RuntimeErrorCode.INVALID_URL, + ngDevMode ? `Invalid URL: ${urlStr}` : urlStr, + ); } if (!originUrl) { @@ -82,7 +89,12 @@ export function resolveUrl( // and we are configured to allow and preserve standard cross-origin protocol-relative requests. if (urlStr.startsWith('//')) { if (!allowProtocolRelative) { - throw new Error(`Protocol relative URLs are not allowed in this context. URL: ${urlStr}`); + throw new RuntimeError( + RuntimeErrorCode.PROTOCOL_RELATIVE_URL_NOT_ALLOWED, + ngDevMode + ? `Protocol relative URLs are not allowed in this context. URL: ${urlStr}` + : urlStr, + ); } return new URL(urlStr, origin); @@ -101,8 +113,11 @@ export function resolveUrl( * Throws a suspicious URL error indicating a security bypass attempt. */ function throwSuspiciousUrlError(urlStr: string): never { - throw new Error( - `URL ${urlStr} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, + throw new RuntimeError( + RuntimeErrorCode.SUSPICIOUS_URL_CHANGE_ORIGIN, + ngDevMode + ? `URL ${urlStr} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.` + : urlStr, ); } diff --git a/packages/platform-server/src/utils.ts b/packages/platform-server/src/utils.ts index 60eaf332a99..53d955d7169 100644 --- a/packages/platform-server/src/utils.ts +++ b/packages/platform-server/src/utils.ts @@ -22,9 +22,11 @@ import { ɵSSR_CONTENT_INTEGRITY_MARKER as SSR_CONTENT_INTEGRITY_MARKER, ɵstartMeasuring as startMeasuring, ɵstopMeasuring as stopMeasuring, + ɵRuntimeError as RuntimeError, } from '@angular/core'; import {BootstrapContext} from '@angular/platform-browser'; +import {RuntimeErrorCode} from './errors'; import {platformServer} from './server'; import {PlatformState} from './platform_state'; import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformConfig} from './tokens'; @@ -384,7 +386,12 @@ function validateAllowedHosts(url: string | undefined, allowedHosts: string[] | const hostname = parsedUrl.hostname; const allowedHostsSet: ReadonlySet = new Set(allowedHosts); if (!isHostAllowed(hostname, allowedHostsSet)) { - throw new Error(`Host ${url} is not allowed. You can configure \`allowedHosts\` option.`); + throw new RuntimeError( + RuntimeErrorCode.HOST_NOT_ALLOWED, + ngDevMode + ? `Host ${url} is not allowed. You can configure \`allowedHosts\` option.` + : url, + ); } } } diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index 36191c1576e..e4b7550f1c0 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -1471,7 +1471,7 @@ class HiddenModule {} next: () => fail('Expected request to fail, but it succeeded.'), error: (err) => { expect(err.message).toBe( - `URL /\\evil.com/api changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, + `NG05703: URL /\\evil.com/api changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, ); }, }); @@ -1598,7 +1598,7 @@ class HiddenModule {} next: () => fail(`Expected request for ${badUrl} to fail, but it succeeded.`), error: (err) => { expect(err.message).toBe( - `URL ${badUrl.trim()} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, + `NG05703: URL ${badUrl.trim()} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, ); }, }); @@ -1621,7 +1621,7 @@ class HiddenModule {} next: () => fail(`Expected request for ${badUrl} to fail, but it succeeded.`), error: (err) => { expect(err.message).toBe( - `URL ${badUrl.trim()} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, + `NG05703: URL ${badUrl.trim()} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, ); }, }); diff --git a/packages/platform-server/test/platform_location_spec.ts b/packages/platform-server/test/platform_location_spec.ts index 090c8e67ac1..0384de69b15 100644 --- a/packages/platform-server/test/platform_location_spec.ts +++ b/packages/platform-server/test/platform_location_spec.ts @@ -147,7 +147,7 @@ import {INITIAL_CONFIG, platformServer} from '@angular/platform-server'; ]); expect(() => platform.injector.get(DOCUMENT)).toThrowError( - `URL /\\attacker.com/deep/path changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, + `NG05703: URL /\\attacker.com/deep/path changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, ); platform.destroy(); }); @@ -164,7 +164,7 @@ import {INITIAL_CONFIG, platformServer} from '@angular/platform-server'; ]); expect(() => platform.injector.get(DOCUMENT)).toThrowError( - `Protocol relative URLs are not allowed in this context. URL: //attacker.com/deep/path`, + `NG05702: Protocol relative URLs are not allowed in this context. URL: //attacker.com/deep/path`, ); platform.destroy(); }); diff --git a/packages/platform-server/test/url_spec.ts b/packages/platform-server/test/url_spec.ts index b348dbc608d..55bf3bd59f0 100644 --- a/packages/platform-server/test/url_spec.ts +++ b/packages/platform-server/test/url_spec.ts @@ -21,7 +21,7 @@ describe('resolveUrl', () => { const urls = ['/\\attacker.com/deep/path', '\\\\attacker.com/deep/path']; for (const url of urls) { expect(() => resolveUrl(url, 'http://test.com')).toThrowError( - `URL ${url} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, + `NG05703: URL ${url} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, ); } }); @@ -51,7 +51,7 @@ describe('resolveUrl', () => { it('should throw on obfuscated protocols attempting to change origin', () => { const url = 'ht\ntp://evil.com/path'; expect(() => resolveUrl(url, 'http://test.com')).toThrowError( - `URL ${url} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, + `NG05703: URL ${url} changed origin unexpectedly. This is suspicious and may indicate a security bypass attempt.`, ); }); });