fix(platform-server): correctly handle absolute relative URLs (#37341)

Previously, we would simply prepend any relative URL with the HREF
for the current route (pulled from document.location). However,
this does not correctly account for the leading slash URLs that
would otherwise be parsed correctly in the browser, or the
presence of a base HREF in the DOM.

Therefore, we use the built-in URL implementation for NodeJS,
which implements the WHATWG standard that's used in the browser.
We also pull the base HREF from the DOM, falling back on the full
HREF as the browser would, to form the correct request URL.

Fixes #37314

PR Close #37341
This commit is contained in:
Adam 2020-05-28 21:33:15 -05:00 committed by atscott
parent 7edb026619
commit 7301e70ddd
3 changed files with 65 additions and 15 deletions

View File

@ -10,13 +10,12 @@
const xhr2: any = require('xhr2'); const xhr2: any = require('xhr2');
import {Injectable, Injector, Provider} from '@angular/core'; import {Injectable, Injector, Provider} from '@angular/core';
import {DOCUMENT} from '@angular/common'; import {PlatformLocation} from '@angular/common';
import {HttpEvent, HttpRequest, HttpHandler, HttpBackend, XhrFactory, ɵHttpInterceptingHandler as HttpInterceptingHandler} from '@angular/common/http'; import {HttpEvent, HttpRequest, HttpHandler, HttpBackend, XhrFactory, ɵHttpInterceptingHandler as HttpInterceptingHandler} from '@angular/common/http';
import {Observable, Observer, Subscription} from 'rxjs'; import {Observable, Observer, Subscription} from 'rxjs';
// @see https://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01#URI-syntax // @see https://www.w3.org/Protocols/HTTP/1.1/draft-ietf-http-v11-spec-01#URI-syntax
const isAbsoluteUrl = /^[a-zA-Z\-\+.]+:\/\//; const isAbsoluteUrl = /^[a-zA-Z\-\+.]+:\/\//;
const FORWARD_SLASH = '/';
@Injectable() @Injectable()
export class ServerXhr implements XhrFactory { export class ServerXhr implements XhrFactory {
@ -105,20 +104,18 @@ export abstract class ZoneMacroTaskWrapper<S, R> {
export class ZoneClientBackend extends export class ZoneClientBackend extends
ZoneMacroTaskWrapper<HttpRequest<any>, HttpEvent<any>> implements HttpBackend { ZoneMacroTaskWrapper<HttpRequest<any>, HttpEvent<any>> implements HttpBackend {
constructor(private backend: HttpBackend, private doc: Document) { constructor(private backend: HttpBackend, private platformLocation: PlatformLocation) {
super(); super();
} }
handle(request: HttpRequest<any>): Observable<HttpEvent<any>> { handle(request: HttpRequest<any>): Observable<HttpEvent<any>> {
const href = this.doc.location.href; const {href, protocol, hostname} = this.platformLocation;
if (!isAbsoluteUrl.test(request.url) && href) { if (!isAbsoluteUrl.test(request.url) && href !== '/') {
const urlParts = Array.from(request.url); const baseHref = this.platformLocation.getBaseHrefFromDOM() || href;
if (request.url[0] === FORWARD_SLASH && href[href.length - 1] === FORWARD_SLASH) { const urlPrefix = `${protocol}//${hostname}`;
urlParts.shift(); const baseUrl = new URL(baseHref, urlPrefix);
} else if (request.url[0] !== FORWARD_SLASH && href[href.length - 1] !== FORWARD_SLASH) { const url = new URL(request.url, baseUrl);
urlParts.splice(0, 0, FORWARD_SLASH); return this.wrap(request.clone({url: url.toString()}));
}
return this.wrap(request.clone({url: href + urlParts.join('')}));
} }
return this.wrap(request); return this.wrap(request);
} }
@ -129,15 +126,15 @@ export class ZoneClientBackend extends
} }
export function zoneWrappedInterceptingHandler( export function zoneWrappedInterceptingHandler(
backend: HttpBackend, injector: Injector, doc: Document) { backend: HttpBackend, injector: Injector, platformLocation: PlatformLocation) {
const realBackend: HttpBackend = new HttpInterceptingHandler(backend, injector); const realBackend: HttpBackend = new HttpInterceptingHandler(backend, injector);
return new ZoneClientBackend(realBackend, doc); return new ZoneClientBackend(realBackend, platformLocation);
} }
export const SERVER_HTTP_PROVIDERS: Provider[] = [ export const SERVER_HTTP_PROVIDERS: Provider[] = [
{provide: XhrFactory, useClass: ServerXhr}, { {provide: XhrFactory, useClass: ServerXhr}, {
provide: HttpHandler, provide: HttpHandler,
useFactory: zoneWrappedInterceptingHandler, useFactory: zoneWrappedInterceptingHandler,
deps: [HttpBackend, Injector, DOCUMENT] deps: [HttpBackend, Injector, PlatformLocation]
} }
]; ];

View File

@ -51,6 +51,7 @@ export class ServerPlatformLocation implements PlatformLocation {
this.pathname = parsedUrl.pathname; this.pathname = parsedUrl.pathname;
this.search = parsedUrl.search; this.search = parsedUrl.search;
this.hash = parsedUrl.hash; this.hash = parsedUrl.hash;
this.href = _doc.location.href;
} }
} }

View File

@ -841,6 +841,58 @@ describe('platform-server integration', () => {
}); });
}); });
it('can make relative HttpClient requests no slashes longer url', async () => {
const platform = platformDynamicServer([{
provide: INITIAL_CONFIG,
useValue: {document: '<app></app>', 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<string>('testing').subscribe((body: string) => {
NgZone.assertInAngularZone();
expect(body).toEqual('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: '<app></app>', 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<string>('/testing').subscribe((body: string) => {
NgZone.assertInAngularZone();
expect(body).toEqual('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: '<base href="http://other"><app></app>', 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<string>('/testing').subscribe((body: string) => {
NgZone.assertInAngularZone();
expect(body).toEqual('success!');
});
mock.expectOne('http://other/testing').flush('success!');
});
});
it('requests are macrotasks', async(() => { it('requests are macrotasks', async(() => {
const platform = platformDynamicServer( const platform = platformDynamicServer(
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]); [{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);