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
This commit is contained in:
Adam Plumer 2020-05-12 10:05:18 -05:00 committed by Misko Hevery
parent ce39755937
commit 9edea0bb75
2 changed files with 72 additions and 7 deletions

View File

@ -10,11 +10,14 @@
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 {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
const isAbsoluteUrl = /^[a-zA-Z\-\+.]+:\/\//;
const FORWARD_SLASH = '/';
@Injectable() @Injectable()
export class ServerXhr implements XhrFactory { export class ServerXhr implements XhrFactory {
build(): XMLHttpRequest { build(): XMLHttpRequest {
@ -102,11 +105,21 @@ 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) { constructor(private backend: HttpBackend, private doc: Document) {
super(); super();
} }
handle(request: HttpRequest<any>): Observable<HttpEvent<any>> { handle(request: HttpRequest<any>): Observable<HttpEvent<any>> {
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); 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); const realBackend: HttpBackend = new HttpInterceptingHandler(backend, injector);
return new ZoneClientBackend(realBackend); return new ZoneClientBackend(realBackend, doc);
} }
export const SERVER_HTTP_PROVIDERS: Provider[] = [ export const SERVER_HTTP_PROVIDERS: Provider[] = [
{provide: XhrFactory, useClass: ServerXhr}, {provide: XhrFactory, useClass: ServerXhr}, {
{provide: HttpHandler, useFactory: zoneWrappedInterceptingHandler, deps: [HttpBackend, Injector]} provide: HttpHandler,
useFactory: zoneWrappedInterceptingHandler,
deps: [HttpBackend, Injector, DOCUMENT]
}
]; ];

View File

@ -793,6 +793,54 @@ describe('platform-server integration', () => {
}); });
})); }));
it('can make relative HttpClient requests', async () => {
const platform = platformDynamicServer([
{provide: INITIAL_CONFIG, useValue: {document: '<app></app>', 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<string>('/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: '<app></app>', 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<string>('/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: '<app></app>', 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<string>('testing').subscribe((body: string) => {
NgZone.assertInAngularZone();
expect(body).toEqual('success!');
});
mock.expectOne('http://localhost/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>'}}]);