From 9edea0bb75c27359bffa6a3062a5a46fc5a2cfcb Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Tue, 12 May 2020 10:05:18 -0500 Subject: [PATCH] feat(platform-server): use absolute URLs from Location for HTTP (#37071) Currently, requests from the server that do not use absolute URLs fail because the server does not have the same fallback mechanism that browser XHR does. This adds that mechanism by pulling the full URL out of the document.location object, if available. PR Close #37071 --- packages/platform-server/src/http.ts | 31 +++++++++--- .../platform-server/test/integration_spec.ts | 48 +++++++++++++++++++ 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/packages/platform-server/src/http.ts b/packages/platform-server/src/http.ts index 981490dc47..a00ce87140 100644 --- a/packages/platform-server/src/http.ts +++ b/packages/platform-server/src/http.ts @@ -10,11 +10,14 @@ const xhr2: any = require('xhr2'); import {Injectable, Injector, Provider} from '@angular/core'; - +import {DOCUMENT} from '@angular/common'; import {HttpEvent, HttpRequest, HttpHandler, HttpBackend, XhrFactory, ɵHttpInterceptingHandler as HttpInterceptingHandler} from '@angular/common/http'; - import {Observable, Observer, Subscription} from 'rxjs'; +// @see https://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01#URI-syntax +const isAbsoluteUrl = /^[a-zA-Z\-\+.]+:\/\//; +const FORWARD_SLASH = '/'; + @Injectable() export class ServerXhr implements XhrFactory { build(): XMLHttpRequest { @@ -102,11 +105,21 @@ export abstract class ZoneMacroTaskWrapper { export class ZoneClientBackend extends ZoneMacroTaskWrapper, HttpEvent> implements HttpBackend { - constructor(private backend: HttpBackend) { + constructor(private backend: HttpBackend, private doc: Document) { super(); } handle(request: HttpRequest): Observable> { + const href = this.doc.location.href; + if (!isAbsoluteUrl.test(request.url) && href) { + const urlParts = Array.from(request.url); + if (request.url[0] === FORWARD_SLASH && href[href.length - 1] === FORWARD_SLASH) { + urlParts.shift(); + } else if (request.url[0] !== FORWARD_SLASH && href[href.length - 1] !== FORWARD_SLASH) { + urlParts.splice(0, 0, FORWARD_SLASH); + } + return this.wrap(request.clone({url: href + urlParts.join('')})); + } return this.wrap(request); } @@ -115,12 +128,16 @@ export class ZoneClientBackend extends } } -export function zoneWrappedInterceptingHandler(backend: HttpBackend, injector: Injector) { +export function zoneWrappedInterceptingHandler( + backend: HttpBackend, injector: Injector, doc: Document) { const realBackend: HttpBackend = new HttpInterceptingHandler(backend, injector); - return new ZoneClientBackend(realBackend); + return new ZoneClientBackend(realBackend, doc); } export const SERVER_HTTP_PROVIDERS: Provider[] = [ - {provide: XhrFactory, useClass: ServerXhr}, - {provide: HttpHandler, useFactory: zoneWrappedInterceptingHandler, deps: [HttpBackend, Injector]} + {provide: XhrFactory, useClass: ServerXhr}, { + provide: HttpHandler, + useFactory: zoneWrappedInterceptingHandler, + deps: [HttpBackend, Injector, DOCUMENT] + } ]; diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index d073987aff..c8fbdc7a19 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -793,6 +793,54 @@ 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!'); + }); + 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!'); + }); + 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!'); + }); + mock.expectOne('http://localhost/testing').flush('success!'); + }); + }); + it('requests are macrotasks', async(() => { const platform = platformDynamicServer( [{provide: INITIAL_CONFIG, useValue: {document: ''}}]);