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:
parent
7edb026619
commit
7301e70ddd
|
@ -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]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>'}}]);
|
||||||
|
|
Loading…
Reference in New Issue