fix(platform-server): Resolve absolute URL from baseUrl (#39334)

This commit fixes a bug when `useAbsoluteUrl` is set to true and
`ServerPlatformLocation` infers the base url from the supplied
`url`. User should explicitly set the `baseUrl` when they turn on
`useAbsoluteUrl`.

Breaking change:
If you use `useAbsoluteUrl` to setup `platform-server`, you now need to
also specify `baseUrl`.
We are intentionally making this a breaking change in a minor release,
because if `useAbsoluteUrl` is set to `true` then the behavior of the
application could be unpredictable, resulting in issues that are hard to
discover but could be affecting production environments.

PR Close #39334
This commit is contained in:
Keen Yee Liau 2020-10-19 15:23:37 -07:00 committed by Andrew Kushnir
parent d33eaa64a2
commit 7768aeb62f
4 changed files with 119 additions and 24 deletions

View File

@ -3,6 +3,7 @@ export declare const BEFORE_APP_SERIALIZED: InjectionToken<(() => void | Promise
export declare const INITIAL_CONFIG: InjectionToken<PlatformConfig>; export declare const INITIAL_CONFIG: InjectionToken<PlatformConfig>;
export declare interface PlatformConfig { export declare interface PlatformConfig {
baseUrl?: string;
document?: string; document?: string;
url?: string; url?: string;
useAbsoluteUrl?: boolean; useAbsoluteUrl?: boolean;

View File

@ -12,7 +12,6 @@ import {Subject} from 'rxjs';
import * as url from 'url'; import * as url from 'url';
import {INITIAL_CONFIG, PlatformConfig} from './tokens'; import {INITIAL_CONFIG, PlatformConfig} from './tokens';
function parseUrl(urlStr: string) { function parseUrl(urlStr: string) {
const parsedUrl = url.parse(urlStr); const parsedUrl = url.parse(urlStr);
return { return {
@ -43,16 +42,28 @@ export class ServerPlatformLocation implements PlatformLocation {
constructor( constructor(
@Inject(DOCUMENT) private _doc: any, @Optional() @Inject(INITIAL_CONFIG) _config: any) { @Inject(DOCUMENT) private _doc: any, @Optional() @Inject(INITIAL_CONFIG) _config: any) {
const config = _config as PlatformConfig | null; const config = _config as PlatformConfig | null;
if (!!config && !!config.url) { if (!config) {
const parsedUrl = parseUrl(config.url); return;
this.hostname = parsedUrl.hostname; }
this.protocol = parsedUrl.protocol; if (config.url) {
this.port = parsedUrl.port; const url = parseUrl(config.url);
this.pathname = parsedUrl.pathname; this.protocol = url.protocol;
this.search = parsedUrl.search; this.hostname = url.hostname;
this.hash = parsedUrl.hash; this.port = url.port;
this.pathname = url.pathname;
this.search = url.search;
this.hash = url.hash;
this.href = _doc.location.href; this.href = _doc.location.href;
} }
if (config.useAbsoluteUrl) {
if (!config.baseUrl) {
throw new Error(`"PlatformConfig.baseUrl" must be set if "useAbsoluteUrl" is true`);
}
const url = parseUrl(config.baseUrl);
this.protocol = url.protocol;
this.hostname = url.hostname;
this.port = url.port;
}
} }
getBaseHrefFromDOM(): string { getBaseHrefFromDOM(): string {

View File

@ -20,19 +20,26 @@ export interface PlatformConfig {
*/ */
document?: string; document?: string;
/** /**
* The URL for the current application state. This is * The URL for the current application state. This is used for initializing
* used for initializing the platform's location and * the platform's location. `protocol`, `hostname`, and `port` will be
* for setting absolute URL resolution for HTTP requests. * overridden if `baseUrl` is set.
* @default none * @default none
*/ */
url?: string; url?: string;
/** /**
* Whether to append the absolute URL to any relative HTTP * Whether to append the absolute URL to any relative HTTP requests. If set to
* requests. If set to true, this logic executes prior to * true, this logic executes prior to any HTTP interceptors that may run later
* any HTTP interceptors that may run later on in the request. * on in the request. `baseUrl` must be supplied if this flag is enabled.
* @default false * @default false
*/ */
useAbsoluteUrl?: boolean; useAbsoluteUrl?: boolean;
/**
* The base URL for resolving absolute URL for HTTP requests. It must be set
* if `useAbsoluteUrl` is true, and must consist of protocol, hostname,
* and optional port. This option has no effect if `useAbsoluteUrl` is not
* enabled.
*/
baseUrl?: string;
} }
/** /**

View File

@ -794,10 +794,62 @@ describe('platform-server integration', () => {
})); }));
describe('relative requests', () => { describe('relative requests', () => {
it('will throw if "useAbsoluteUrl" is true but "baseUrl" is not provided', async () => {
const platform = platformDynamicServer([{
provide: INITIAL_CONFIG,
useValue: {
document: '<app></app>',
url: 'http://localhost',
useAbsoluteUrl: true,
},
}]);
const appRef = await platform.bootstrapModule(HttpClientExampleModule);
expect(() => appRef.injector.get(PlatformLocation))
.toThrowError(/"PlatformConfig\.baseUrl" must be set if "useAbsoluteUrl" is true/);
});
it('will resolve absolute url using "baseUrl"', async () => {
const platform = platformDynamicServer([{
provide: INITIAL_CONFIG,
useValue: {
document: '<app></app>',
url: 'http://localhost',
useAbsoluteUrl: true,
baseUrl: 'https://angular.io:8080',
},
}]);
const appRef = await platform.bootstrapModule(HttpClientExampleModule);
const location = appRef.injector.get(PlatformLocation);
expect(location.protocol).toBe('https:');
expect(location.hostname).toBe('angular.io');
expect(location.port).toBe('8080');
});
it('"baseUrl" has no effect if "useAbsoluteUrl" is not enabled', async () => {
const platform = platformDynamicServer([{
provide: INITIAL_CONFIG,
useValue: {
document: '<app></app>',
url: 'http://localhost',
baseUrl: 'https://angular.io:8080',
},
}]);
const appRef = await platform.bootstrapModule(HttpClientExampleModule);
const location = appRef.injector.get(PlatformLocation);
expect(location.protocol).toBe('http:');
expect(location.hostname).toBe('localhost');
expect(location.port).toBe('');
});
it('correctly maps to absolute URL request with base config', async () => { it('correctly maps to absolute URL request with base config', async () => {
const platform = platformDynamicServer([{ const platform = platformDynamicServer([{
provide: INITIAL_CONFIG, provide: INITIAL_CONFIG,
useValue: {document: '<app></app>', url: 'http://localhost', useAbsoluteUrl: true} useValue: {
document: '<app></app>',
url: 'http://localhost',
baseUrl: 'http://localhost',
useAbsoluteUrl: true,
}
}]); }]);
const ref = await platform.bootstrapModule(HttpClientExampleModule); const ref = await platform.bootstrapModule(HttpClientExampleModule);
const mock = ref.injector.get(HttpTestingController) as HttpTestingController; const mock = ref.injector.get(HttpTestingController) as HttpTestingController;
@ -831,7 +883,12 @@ describe('platform-server integration', () => {
it('correctly maps to absolute URL request with port', async () => { it('correctly maps to absolute URL request with port', async () => {
const platform = platformDynamicServer([{ const platform = platformDynamicServer([{
provide: INITIAL_CONFIG, provide: INITIAL_CONFIG,
useValue: {document: '<app></app>', url: 'http://localhost:5000', useAbsoluteUrl: true} useValue: {
document: '<app></app>',
url: 'http://localhost:5000',
baseUrl: 'http://localhost',
useAbsoluteUrl: true,
}
}]); }]);
const ref = await platform.bootstrapModule(HttpClientExampleModule); const ref = await platform.bootstrapModule(HttpClientExampleModule);
const mock = ref.injector.get(HttpTestingController) as HttpTestingController; const mock = ref.injector.get(HttpTestingController) as HttpTestingController;
@ -848,7 +905,12 @@ describe('platform-server integration', () => {
it('correctly maps to absolute URL request with two slashes', async () => { it('correctly maps to absolute URL request with two slashes', async () => {
const platform = platformDynamicServer([{ const platform = platformDynamicServer([{
provide: INITIAL_CONFIG, provide: INITIAL_CONFIG,
useValue: {document: '<app></app>', url: 'http://localhost/', useAbsoluteUrl: true} useValue: {
document: '<app></app>',
url: 'http://localhost/',
baseUrl: 'http://localhost',
useAbsoluteUrl: true,
}
}]); }]);
const ref = await platform.bootstrapModule(HttpClientExampleModule); const ref = await platform.bootstrapModule(HttpClientExampleModule);
const mock = ref.injector.get(HttpTestingController) as HttpTestingController; const mock = ref.injector.get(HttpTestingController) as HttpTestingController;
@ -865,7 +927,12 @@ describe('platform-server integration', () => {
it('correctly maps to absolute URL request with no slashes', async () => { it('correctly maps to absolute URL request with no slashes', async () => {
const platform = platformDynamicServer([{ const platform = platformDynamicServer([{
provide: INITIAL_CONFIG, provide: INITIAL_CONFIG,
useValue: {document: '<app></app>', url: 'http://localhost', useAbsoluteUrl: true} useValue: {
document: '<app></app>',
url: 'http://localhost',
baseUrl: 'http://localhost',
useAbsoluteUrl: true,
}
}]); }]);
const ref = await platform.bootstrapModule(HttpClientExampleModule); const ref = await platform.bootstrapModule(HttpClientExampleModule);
const mock = ref.injector.get(HttpTestingController) as HttpTestingController; const mock = ref.injector.get(HttpTestingController) as HttpTestingController;
@ -882,8 +949,12 @@ describe('platform-server integration', () => {
it('correctly maps to absolute URL request with longer url and no slashes', async () => { it('correctly maps to absolute URL request with longer url and no slashes', async () => {
const platform = platformDynamicServer([{ const platform = platformDynamicServer([{
provide: INITIAL_CONFIG, provide: INITIAL_CONFIG,
useValue: useValue: {
{document: '<app></app>', url: 'http://localhost/path/page', useAbsoluteUrl: true} document: '<app></app>',
url: 'http://localhost/path/page',
baseUrl: 'http://localhost',
useAbsoluteUrl: true,
}
}]); }]);
const ref = await platform.bootstrapModule(HttpClientExampleModule); const ref = await platform.bootstrapModule(HttpClientExampleModule);
const mock = ref.injector.get(HttpTestingController) as HttpTestingController; const mock = ref.injector.get(HttpTestingController) as HttpTestingController;
@ -900,8 +971,12 @@ describe('platform-server integration', () => {
it('correctly maps to absolute URL request with longer url and slashes', async () => { it('correctly maps to absolute URL request with longer url and slashes', async () => {
const platform = platformDynamicServer([{ const platform = platformDynamicServer([{
provide: INITIAL_CONFIG, provide: INITIAL_CONFIG,
useValue: useValue: {
{document: '<app></app>', url: 'http://localhost/path/page', useAbsoluteUrl: true} document: '<app></app>',
url: 'http://localhost/path/page',
baseUrl: 'http://localhost',
useAbsoluteUrl: true,
}
}]); }]);
const ref = await platform.bootstrapModule(HttpClientExampleModule); const ref = await platform.bootstrapModule(HttpClientExampleModule);
const mock = ref.injector.get(HttpTestingController) as HttpTestingController; const mock = ref.injector.get(HttpTestingController) as HttpTestingController;
@ -922,7 +997,8 @@ describe('platform-server integration', () => {
useValue: { useValue: {
document: '<base href="http://other"><app></app>', document: '<base href="http://other"><app></app>',
url: 'http://localhost/path/page', url: 'http://localhost/path/page',
useAbsoluteUrl: true baseUrl: 'http://localhost',
useAbsoluteUrl: true,
} }
}]); }]);
const ref = await platform.bootstrapModule(HttpClientExampleModule); const ref = await platform.bootstrapModule(HttpClientExampleModule);