From d37049a2a2636fc40ebc81b57079f87838691109 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 11 Jun 2020 11:42:31 -0500 Subject: [PATCH] feat(platform-server): add option for absolute URL HTTP support (#37539) In version 10.0.0-next.8, we introduced absolute URL support for server-based HTTP requests, so long as the fully-resolved URL was provided in the initial config. However, doing so represents a breaking change for users who already have their own interceptors to model this functionality, since our logic executes before all interceptors fire on a request. See original PR #37071. Therefore, we introduce a flag to make this change consistent with v9 behavior, allowing users to opt in to this new behavior. This commit also fixes two issues with the previous implementation: 1. if the server was initiated with a relative URL, the absolute URL construction would fail because needed components were empty 2. if the user's absolute URL was on a port, the port would not be included PR Close #37539 --- .../platform-server/platform-server.d.ts | 1 + packages/platform-server/src/http.ts | 19 +- packages/platform-server/src/tokens.ts | 17 ++ .../platform-server/test/integration_spec.ts | 207 +++++++++++------- 4 files changed, 156 insertions(+), 88 deletions(-) diff --git a/goldens/public-api/platform-server/platform-server.d.ts b/goldens/public-api/platform-server/platform-server.d.ts index c7c635e5f4..7db0b77e97 100644 --- a/goldens/public-api/platform-server/platform-server.d.ts +++ b/goldens/public-api/platform-server/platform-server.d.ts @@ -5,6 +5,7 @@ export declare const INITIAL_CONFIG: InjectionToken; export declare interface PlatformConfig { document?: string; url?: string; + useAbsoluteUrl?: boolean; } export declare const platformDynamicServer: (extraProviders?: StaticProvider[] | undefined) => PlatformRef; diff --git a/packages/platform-server/src/http.ts b/packages/platform-server/src/http.ts index d165be953b..be178f3765 100644 --- a/packages/platform-server/src/http.ts +++ b/packages/platform-server/src/http.ts @@ -5,6 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {INITIAL_CONFIG, PlatformConfig} from './tokens'; const xhr2: any = require('xhr2'); @@ -104,15 +105,18 @@ export abstract class ZoneMacroTaskWrapper { export class ZoneClientBackend extends ZoneMacroTaskWrapper, HttpEvent> implements HttpBackend { - constructor(private backend: HttpBackend, private platformLocation: PlatformLocation) { + constructor( + private backend: HttpBackend, private platformLocation: PlatformLocation, + private config: PlatformConfig) { super(); } handle(request: HttpRequest): Observable> { - const {href, protocol, hostname} = this.platformLocation; - if (!isAbsoluteUrl.test(request.url) && href !== '/') { + const {href, protocol, hostname, port} = this.platformLocation; + if (this.config.useAbsoluteUrl && !isAbsoluteUrl.test(request.url) && + isAbsoluteUrl.test(href)) { const baseHref = this.platformLocation.getBaseHrefFromDOM() || href; - const urlPrefix = `${protocol}//${hostname}`; + const urlPrefix = `${protocol}//${hostname}` + (port ? `:${port}` : ''); const baseUrl = new URL(baseHref, urlPrefix); const url = new URL(request.url, baseUrl); return this.wrap(request.clone({url: url.toString()})); @@ -126,15 +130,16 @@ export class ZoneClientBackend extends } export function zoneWrappedInterceptingHandler( - backend: HttpBackend, injector: Injector, platformLocation: PlatformLocation) { + backend: HttpBackend, injector: Injector, platformLocation: PlatformLocation, + config: PlatformConfig) { const realBackend: HttpBackend = new HttpInterceptingHandler(backend, injector); - return new ZoneClientBackend(realBackend, platformLocation); + return new ZoneClientBackend(realBackend, platformLocation, config); } export const SERVER_HTTP_PROVIDERS: Provider[] = [ {provide: XhrFactory, useClass: ServerXhr}, { provide: HttpHandler, useFactory: zoneWrappedInterceptingHandler, - deps: [HttpBackend, Injector, PlatformLocation] + deps: [HttpBackend, Injector, PlatformLocation, INITIAL_CONFIG] } ]; diff --git a/packages/platform-server/src/tokens.ts b/packages/platform-server/src/tokens.ts index eeaa33c454..b5942985d1 100644 --- a/packages/platform-server/src/tokens.ts +++ b/packages/platform-server/src/tokens.ts @@ -14,8 +14,25 @@ import {InjectionToken} from '@angular/core'; * @publicApi */ export interface PlatformConfig { + /** + * The initial DOM to use to bootstrap the server application. + * @default create a new DOM using Domino + */ document?: string; + /** + * The URL for the current application state. This is + * used for initializing the platform's location and + * for setting absolute URL resolution for HTTP requests. + * @default none + */ url?: string; + /** + * Whether to append the absolute URL to any relative HTTP + * requests. If set to true, this logic executes prior to + * any HTTP interceptors that may run later on in the request. + * @default false + */ + useAbsoluteUrl?: boolean; } /** diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index db3d334e55..5703c00885 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -793,104 +793,149 @@ describe('platform-server integration', () => { }); })); - it('can make relative HttpClient requests', async () => { - const platform = platformDynamicServer([ - {provide: INITIAL_CONFIG, useValue: {document: '', url: 'http://localhost'}} - ]); - const ref = await platform.bootstrapModule(HttpClientExampleModule); - const mock = ref.injector.get(HttpTestingController) as HttpTestingController; - const http = ref.injector.get(HttpClient); - ref.injector.get(NgZone).run(() => { - http.get('/testing').subscribe((body: string) => { - NgZone.assertInAngularZone(); - expect(body).toEqual('success!'); + describe('relative requests', () => { + it('correctly maps to absolute URL request with base config', async () => { + const platform = platformDynamicServer([{ + provide: INITIAL_CONFIG, + useValue: {document: '', url: 'http://localhost', useAbsoluteUrl: true} + }]); + const ref = await platform.bootstrapModule(HttpClientExampleModule); + const mock = ref.injector.get(HttpTestingController) as HttpTestingController; + const http = ref.injector.get(HttpClient); + ref.injector.get(NgZone).run(() => { + http.get('/testing').subscribe((body: string) => { + NgZone.assertInAngularZone(); + expect(body).toEqual('success!'); + }); + mock.expectOne('http://localhost/testing').flush('success!'); }); - mock.expectOne('http://localhost/testing').flush('success!'); }); - }); - it('can make relative HttpClient requests two slashes', async () => { - const platform = platformDynamicServer([ - {provide: INITIAL_CONFIG, useValue: {document: '', url: 'http://localhost/'}} - ]); - const ref = await platform.bootstrapModule(HttpClientExampleModule); - const mock = ref.injector.get(HttpTestingController) as HttpTestingController; - const http = ref.injector.get(HttpClient); - ref.injector.get(NgZone).run(() => { - http.get('/testing').subscribe((body: string) => { - NgZone.assertInAngularZone(); - expect(body).toEqual('success!'); + it('uses default URL behavior when not enabled', async () => { + const platform = platformDynamicServer([{ + provide: INITIAL_CONFIG, + useValue: {document: '', url: 'http://localhost', useAbsoluteUrl: false} + }]); + const ref = await platform.bootstrapModule(HttpClientExampleModule); + const mock = ref.injector.get(HttpTestingController) as HttpTestingController; + const http = ref.injector.get(HttpClient); + ref.injector.get(NgZone).run(() => { + http.get('/testing').subscribe(() => {}, (body: string) => { + NgZone.assertInAngularZone(); + expect(body).toEqual('error'); + }); + mock.expectOne('/testing').flush('error'); }); - mock.expectOne('http://localhost/testing').flush('success!'); }); - }); - it('can make relative HttpClient requests no slashes', async () => { - const platform = platformDynamicServer([ - {provide: INITIAL_CONFIG, useValue: {document: '', url: 'http://localhost'}} - ]); - const ref = await platform.bootstrapModule(HttpClientExampleModule); - const mock = ref.injector.get(HttpTestingController) as HttpTestingController; - const http = ref.injector.get(HttpClient); - ref.injector.get(NgZone).run(() => { - http.get('testing').subscribe((body: string) => { - NgZone.assertInAngularZone(); - expect(body).toEqual('success!'); + it('correctly maps to absolute URL request with port', async () => { + const platform = platformDynamicServer([{ + provide: INITIAL_CONFIG, + useValue: {document: '', url: 'http://localhost:5000', useAbsoluteUrl: true} + }]); + const ref = await platform.bootstrapModule(HttpClientExampleModule); + const mock = ref.injector.get(HttpTestingController) as HttpTestingController; + const http = ref.injector.get(HttpClient); + ref.injector.get(NgZone).run(() => { + http.get('/testing').subscribe((body: string) => { + NgZone.assertInAngularZone(); + expect(body).toEqual('success!'); + }); + mock.expectOne('http://localhost:5000/testing').flush('success!'); }); - mock.expectOne('http://localhost/testing').flush('success!'); }); - }); - it('can make relative HttpClient requests no slashes longer url', async () => { - const platform = platformDynamicServer([{ - provide: INITIAL_CONFIG, - useValue: {document: '', url: 'http://localhost/path/page'} - }]); - const ref = await platform.bootstrapModule(HttpClientExampleModule); - const mock = ref.injector.get(HttpTestingController) as HttpTestingController; - const http = ref.injector.get(HttpClient); - ref.injector.get(NgZone).run(() => { - http.get('testing').subscribe((body: string) => { - NgZone.assertInAngularZone(); - expect(body).toEqual('success!'); + it('correctly maps to absolute URL request with two slashes', async () => { + const platform = platformDynamicServer([{ + provide: INITIAL_CONFIG, + useValue: {document: '', url: 'http://localhost/', useAbsoluteUrl: true} + }]); + const ref = await platform.bootstrapModule(HttpClientExampleModule); + const mock = ref.injector.get(HttpTestingController) as HttpTestingController; + const http = ref.injector.get(HttpClient); + ref.injector.get(NgZone).run(() => { + http.get('/testing').subscribe((body: string) => { + NgZone.assertInAngularZone(); + expect(body).toEqual('success!'); + }); + mock.expectOne('http://localhost/testing').flush('success!'); }); - mock.expectOne('http://localhost/path/testing').flush('success!'); }); - }); - it('can make relative HttpClient requests slashes longer url', async () => { - const platform = platformDynamicServer([{ - provide: INITIAL_CONFIG, - useValue: {document: '', url: 'http://localhost/path/page'} - }]); - const ref = await platform.bootstrapModule(HttpClientExampleModule); - const mock = ref.injector.get(HttpTestingController) as HttpTestingController; - const http = ref.injector.get(HttpClient); - ref.injector.get(NgZone).run(() => { - http.get('/testing').subscribe((body: string) => { - NgZone.assertInAngularZone(); - expect(body).toEqual('success!'); + it('correctly maps to absolute URL request with no slashes', async () => { + const platform = platformDynamicServer([{ + provide: INITIAL_CONFIG, + useValue: {document: '', url: 'http://localhost', useAbsoluteUrl: true} + }]); + const ref = await platform.bootstrapModule(HttpClientExampleModule); + const mock = ref.injector.get(HttpTestingController) as HttpTestingController; + const http = ref.injector.get(HttpClient); + ref.injector.get(NgZone).run(() => { + http.get('testing').subscribe((body: string) => { + NgZone.assertInAngularZone(); + expect(body).toEqual('success!'); + }); + mock.expectOne('http://localhost/testing').flush('success!'); }); - mock.expectOne('http://localhost/testing').flush('success!'); }); - }); - it('can make relative HttpClient requests slashes longer url with base href', async () => { - const platform = platformDynamicServer([{ - provide: INITIAL_CONFIG, - useValue: - {document: '', url: 'http://localhost/path/page'} - }]); - const ref = await platform.bootstrapModule(HttpClientExampleModule); - const mock = ref.injector.get(HttpTestingController) as HttpTestingController; - const http = ref.injector.get(HttpClient); - ref.injector.get(NgZone).run(() => { - http.get('/testing').subscribe((body: string) => { - NgZone.assertInAngularZone(); - expect(body).toEqual('success!'); + it('correctly maps to absolute URL request with longer url and no slashes', async () => { + const platform = platformDynamicServer([{ + provide: INITIAL_CONFIG, + useValue: + {document: '', url: 'http://localhost/path/page', useAbsoluteUrl: true} + }]); + const ref = await platform.bootstrapModule(HttpClientExampleModule); + const mock = ref.injector.get(HttpTestingController) as HttpTestingController; + const http = ref.injector.get(HttpClient); + ref.injector.get(NgZone).run(() => { + http.get('testing').subscribe((body: string) => { + NgZone.assertInAngularZone(); + expect(body).toEqual('success!'); + }); + mock.expectOne('http://localhost/path/testing').flush('success!'); }); - mock.expectOne('http://other/testing').flush('success!'); }); + + it('correctly maps to absolute URL request with longer url and slashes', async () => { + const platform = platformDynamicServer([{ + provide: INITIAL_CONFIG, + useValue: + {document: '', url: 'http://localhost/path/page', useAbsoluteUrl: true} + }]); + const ref = await platform.bootstrapModule(HttpClientExampleModule); + const mock = ref.injector.get(HttpTestingController) as HttpTestingController; + const http = ref.injector.get(HttpClient); + ref.injector.get(NgZone).run(() => { + http.get('/testing').subscribe((body: string) => { + NgZone.assertInAngularZone(); + expect(body).toEqual('success!'); + }); + mock.expectOne('http://localhost/testing').flush('success!'); + }); + }); + + it('correctly maps to absolute URL request with longer url, slashes, and base href', + async () => { + const platform = platformDynamicServer([{ + provide: INITIAL_CONFIG, + useValue: { + document: '', + url: 'http://localhost/path/page', + useAbsoluteUrl: true + } + }]); + const ref = await platform.bootstrapModule(HttpClientExampleModule); + const mock = ref.injector.get(HttpTestingController) as HttpTestingController; + const http = ref.injector.get(HttpClient); + ref.injector.get(NgZone).run(() => { + http.get('/testing').subscribe((body: string) => { + NgZone.assertInAngularZone(); + expect(body).toEqual('success!'); + }); + mock.expectOne('http://other/testing').flush('success!'); + }); + }); }); it('requests are macrotasks', async(() => {