Skip to content

Commit 405ec8c

Browse files
alan-agius4dylhunn
authored andcommitted
fix(platform-server): resolve relative requests URL (angular#52326)
Prior to this commit relative HTTP requests were not being resolved to absolute even thought the behaviour is documented in https://angular.io/guide/universal#using-absolute-urls-for-http-data-requests-on-the-server. This caused relative HTTP requests to fail when done on the server because of missing request context. This change is also required to eventually support HTTP requests handled during prerendering (SSG). Closes angular#51626 PR Close angular#52326
1 parent e6affef commit 405ec8c

8 files changed

Lines changed: 89 additions & 9 deletions

File tree

integration/platform-server/projects/ngmodule/server.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,18 @@ app.get('/api-2', (req, res) => {
3838

3939
// All regular routes use the Universal engine
4040
app.get('*', (req, res) => {
41+
const { protocol, originalUrl, baseUrl, headers } = req;
42+
4143
renderModule(AppServerModule, {
4244
document: indexHtml,
43-
url: req.url,
44-
extraProviders: [{provide: APP_BASE_HREF, useValue: req.baseUrl}],
45+
url: `${protocol}://${headers.host}${originalUrl}`,
46+
extraProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
4547
}).then((response: string) => {
4648
res.send(response);
4749
});
4850
});
4951

52+
5053
app.listen(4206, () => {
5154
console.log('Server listening on port 4206!');
5255
});

integration/platform-server/projects/ngmodule/src/app/http-transferstate-lazy/http-transfer-state.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class TransferStateComponent implements OnInit {
2929

3030
ngOnInit(): void {
3131
// Test that HTTP cache works when HTTP call is made in a lifecycle hook.
32-
this.httpClient.get<any>('http://localhost:4206/api-2').subscribe((response) => {
32+
this.httpClient.get<any>('/api-2').subscribe((response) => {
3333
this.responseTwo = response.data;
3434
});
3535
}

integration/platform-server/projects/standalone/server.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ app.get('/api-2', (req, res) => {
3838

3939
// All regular routes use the Universal engine
4040
app.get('*', (req, res) => {
41+
const { protocol, originalUrl, baseUrl, headers } = req;
42+
4143
renderApplication(bootstrap, {
4244
document: indexHtml,
43-
url: req.url,
44-
platformProviders: [{provide: APP_BASE_HREF, useValue: req.baseUrl}],
45+
url: `${protocol}://${headers.host}${originalUrl}`,
46+
platformProviders: [{provide: APP_BASE_HREF, useValue: baseUrl}],
4547
}).then((response: string) => {
4648
res.send(response);
4749
});

integration/platform-server/projects/standalone/src/app/http-transferstate-lazy/http-transfer-state.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export class TransferStateComponent implements OnInit {
3131

3232
ngOnInit(): void {
3333
// Test that HTTP cache works when HTTP call is made in a lifecycle hook.
34-
this.httpClient.get<any>('http://localhost:4206/api-2').subscribe((response) => {
34+
this.httpClient.get<any>('/api-2').subscribe((response) => {
3535
this.responseTwo = response.data;
3636
});
3737
}

packages/common/http/public_api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,6 @@ export {HttpDownloadProgressEvent, HttpErrorResponse, HttpEvent, HttpEventType,
2121
export {HttpTransferCacheOptions, withHttpTransferCache as ɵwithHttpTransferCache} from './src/transfer_cache';
2222
export {HttpXhrBackend} from './src/xhr';
2323
export {HttpXsrfTokenExtractor} from './src/xsrf';
24+
25+
// Private exports
26+
export * from './src/private_export';
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export {HTTP_ROOT_INTERCEPTOR_FNS as ɵHTTP_ROOT_INTERCEPTOR_FNS} from './interceptor';

packages/platform-server/src/http.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {XhrFactory} from '@angular/common';
10-
import {Injectable, Provider} from '@angular/core';
9+
import {PlatformLocation, XhrFactory} from '@angular/common';
10+
import {HttpEvent, HttpHandlerFn, HttpRequest, ɵHTTP_ROOT_INTERCEPTOR_FNS as HTTP_ROOT_INTERCEPTOR_FNS} from '@angular/common/http';
11+
import {inject, Injectable, Provider} from '@angular/core';
12+
import {Observable} from 'rxjs';
1113

1214
@Injectable()
1315
export class ServerXhr implements XhrFactory {
@@ -34,7 +36,31 @@ export class ServerXhr implements XhrFactory {
3436
}
3537
}
3638

39+
function relativeUrlsTransformerInterceptorFn(
40+
request: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
41+
const platformLocation = inject(PlatformLocation);
42+
const {href, protocol, hostname, port} = platformLocation;
43+
if (!protocol.startsWith('http')) {
44+
return next(request);
45+
}
46+
47+
let urlPrefix = `${protocol}//${hostname}`;
48+
if (port) {
49+
urlPrefix += `:${port}`;
50+
}
51+
52+
const baseHref = platformLocation.getBaseHrefFromDOM() || href;
53+
const baseUrl = new URL(baseHref, urlPrefix);
54+
const newUrl = new URL(request.url, baseUrl).toString();
55+
56+
return next(request.clone({url: newUrl}));
57+
}
3758

3859
export const SERVER_HTTP_PROVIDERS: Provider[] = [
3960
{provide: XhrFactory, useClass: ServerXhr},
61+
{
62+
provide: HTTP_ROOT_INTERCEPTOR_FNS,
63+
useValue: relativeUrlsTransformerInterceptorFn,
64+
multi: true,
65+
},
4066
];

packages/platform-server/test/integration_spec.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {animate, AnimationBuilder, state, style, transition, trigger} from '@ang
1111
import {DOCUMENT, isPlatformServer, PlatformLocation, ɵgetDOM as getDOM} from '@angular/common';
1212
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
1313
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
14-
import {ApplicationConfig, ApplicationRef, Component, destroyPlatform, EnvironmentProviders, HostListener, Inject, inject as coreInject, Injectable, Input, makeStateKey, mergeApplicationConfig, NgModule, NgZone, PLATFORM_ID, Provider, TransferState, Type, ViewEncapsulation, ɵwhenStable as whenStable} from '@angular/core';
14+
import {ApplicationConfig, ApplicationRef, Component, destroyPlatform, EnvironmentProviders, HostListener, Inject, inject as coreInject, Injectable, Input, makeStateKey, mergeApplicationConfig, NgModule, NgModuleRef, NgZone, PLATFORM_ID, Provider, TransferState, Type, ViewEncapsulation, ɵwhenStable as whenStable} from '@angular/core';
1515
import {SSR_CONTENT_INTEGRITY_MARKER} from '@angular/core/src/hydration/utils';
1616
import {InitialRenderPendingTasks} from '@angular/core/src/initial_render_pending_tasks';
1717
import {TestBed} from '@angular/core/testing';
@@ -1147,6 +1147,43 @@ describe('platform-server integration', () => {
11471147
});
11481148
});
11491149
});
1150+
1151+
describe(`given 'url' is provided in 'INITIAL_CONFIG'`, () => {
1152+
let mock: HttpTestingController;
1153+
let ref: NgModuleRef<HttpInterceptorExampleModule>;
1154+
let http: HttpClient;
1155+
1156+
beforeEach(async () => {
1157+
const platform = platformServer([{
1158+
provide: INITIAL_CONFIG,
1159+
useValue: {document: '<app></app>', url: 'http://localhost:4000/foo'}
1160+
}]);
1161+
1162+
ref = await platform.bootstrapModule(HttpInterceptorExampleModule);
1163+
mock = ref.injector.get(HttpTestingController);
1164+
http = ref.injector.get(HttpClient);
1165+
});
1166+
1167+
it('should resolve relative request URLs to absolute', async () => {
1168+
ref.injector.get(NgZone).run(() => {
1169+
http.get('/testing').subscribe(body => {
1170+
NgZone.assertInAngularZone();
1171+
expect(body).toEqual('success!');
1172+
});
1173+
mock.expectOne('http://localhost:4000/testing').flush('success!');
1174+
});
1175+
});
1176+
1177+
it(`should not replace the baseUrl of a request when it's absolute`, async () => {
1178+
ref.injector.get(NgZone).run(() => {
1179+
http.get('http://localhost/testing').subscribe(body => {
1180+
NgZone.assertInAngularZone();
1181+
expect(body).toEqual('success!');
1182+
});
1183+
mock.expectOne('http://localhost/testing').flush('success!');
1184+
});
1185+
});
1186+
});
11501187
});
11511188
});
11521189
})();

0 commit comments

Comments
 (0)