mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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
This commit is contained in:
@@ -63,6 +63,24 @@ export function renderModule<T>(moduleType: Type<T>, options: {
|
||||
allowedHosts?: Readonly<string>[];
|
||||
}): Promise<string>;
|
||||
|
||||
// @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)
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string> = 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user