Skip to content

Commit 8ca6681

Browse files
committed
[DURACOM-288] Provide a setting to use a different REST url during SSR execution
# Conflicts: # src/app/app.config.ts # src/app/core/services/server-hard-redirect.service.spec.ts # src/app/core/services/server-hard-redirect.service.ts # src/app/thumbnail/thumbnail.component.ts # src/modules/app/browser-init.service.ts # src/modules/app/server-init.service.ts
1 parent 9506412 commit 8ca6681

10 files changed

Lines changed: 315 additions & 27 deletions

server.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ let anonymousCache: LRU<string, any>;
7979
// extend environment with app config for server
8080
extendEnvironmentWithAppConfig(environment, appConfig);
8181

82+
// The REST server base URL
83+
const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl;
84+
8285
// The Express app is exported so that it can be used by serverless Functions.
8386
export function app() {
8487

@@ -176,7 +179,7 @@ export function app() {
176179
* Proxy the sitemaps
177180
*/
178181
router.use('/sitemap**', createProxyMiddleware({
179-
target: `${environment.rest.baseUrl}/sitemaps`,
182+
target: `${REST_BASE_URL}/sitemaps`,
180183
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
181184
changeOrigin: true
182185
}));
@@ -185,7 +188,7 @@ export function app() {
185188
* Proxy the linksets
186189
*/
187190
router.use('/signposting**', createProxyMiddleware({
188-
target: `${environment.rest.baseUrl}`,
191+
target: `${REST_BASE_URL}`,
189192
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
190193
changeOrigin: true
191194
}));
@@ -621,7 +624,7 @@ function start() {
621624
* The callback function to serve health check requests
622625
*/
623626
function healthCheck(req, res) {
624-
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
627+
const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
625628
axios.get(baseUrl)
626629
.then((response) => {
627630
res.status(response.status).send(response.data);

src/app/app.module.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { EagerThemesModule } from '../themes/eager-themes.module';
3030
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
3131
import { StoreDevModules } from '../config/store/devtools';
3232
import { RootModule } from './root.module';
33+
import { DspaceRestInterceptor } from './core/dspace-rest/dspace-rest.interceptor';
3334

3435
export function getConfig() {
3536
return environment;
@@ -103,6 +104,12 @@ const PROVIDERS = [
103104
useClass: LogInterceptor,
104105
multi: true
105106
},
107+
// register DspaceRestInterceptor as HttpInterceptor
108+
{
109+
provide: HTTP_INTERCEPTORS,
110+
useClass: DspaceRestInterceptor,
111+
multi: true
112+
},
106113
// register the dynamic matcher used by form. MUST be provided by the app module
107114
...DYNAMIC_MATCHER_PROVIDERS,
108115
];
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import {
2+
HTTP_INTERCEPTORS,
3+
HttpClient,
4+
} from '@angular/common/http';
5+
import {
6+
HttpClientTestingModule,
7+
HttpTestingController,
8+
} from '@angular/common/http/testing';
9+
import { PLATFORM_ID } from '@angular/core';
10+
import { TestBed } from '@angular/core/testing';
11+
12+
import {
13+
APP_CONFIG,
14+
AppConfig,
15+
} from '../../../config/app-config.interface';
16+
import { DspaceRestInterceptor } from './dspace-rest.interceptor';
17+
import { DspaceRestService } from './dspace-rest.service';
18+
19+
describe('DspaceRestInterceptor', () => {
20+
let httpMock: HttpTestingController;
21+
let httpClient: HttpClient;
22+
const appConfig: Partial<AppConfig> = {
23+
rest: {
24+
ssl: false,
25+
host: 'localhost',
26+
port: 8080,
27+
nameSpace: '/server',
28+
baseUrl: 'http://api.example.com/server',
29+
},
30+
};
31+
const appConfigWithSSR: Partial<AppConfig> = {
32+
rest: {
33+
ssl: false,
34+
host: 'localhost',
35+
port: 8080,
36+
nameSpace: '/server',
37+
baseUrl: 'http://api.example.com/server',
38+
ssrBaseUrl: 'http://ssr.example.com/server',
39+
},
40+
};
41+
42+
describe('When SSR base URL is not set ', () => {
43+
describe('and it\'s in the browser', () => {
44+
beforeEach(() => {
45+
TestBed.configureTestingModule({
46+
imports: [HttpClientTestingModule],
47+
providers: [
48+
DspaceRestService,
49+
{
50+
provide: HTTP_INTERCEPTORS,
51+
useClass: DspaceRestInterceptor,
52+
multi: true,
53+
},
54+
{ provide: APP_CONFIG, useValue: appConfig },
55+
{ provide: PLATFORM_ID, useValue: 'browser' },
56+
],
57+
});
58+
59+
httpMock = TestBed.inject(HttpTestingController);
60+
httpClient = TestBed.inject(HttpClient);
61+
});
62+
63+
it('should not modify the request', () => {
64+
const url = 'http://api.example.com/server/items';
65+
httpClient.get(url).subscribe((response) => {
66+
expect(response).toBeTruthy();
67+
});
68+
69+
const req = httpMock.expectOne(url);
70+
expect(req.request.url).toBe(url);
71+
req.flush({});
72+
httpMock.verify();
73+
});
74+
});
75+
76+
describe('and it\'s in SSR mode', () => {
77+
beforeEach(() => {
78+
TestBed.configureTestingModule({
79+
imports: [HttpClientTestingModule],
80+
providers: [
81+
DspaceRestService,
82+
{
83+
provide: HTTP_INTERCEPTORS,
84+
useClass: DspaceRestInterceptor,
85+
multi: true,
86+
},
87+
{ provide: APP_CONFIG, useValue: appConfig },
88+
{ provide: PLATFORM_ID, useValue: 'server' },
89+
],
90+
});
91+
92+
httpMock = TestBed.inject(HttpTestingController);
93+
httpClient = TestBed.inject(HttpClient);
94+
});
95+
96+
it('should not replace the base URL', () => {
97+
const url = 'http://api.example.com/server/items';
98+
99+
httpClient.get(url).subscribe((response) => {
100+
expect(response).toBeTruthy();
101+
});
102+
103+
const req = httpMock.expectOne(url);
104+
expect(req.request.url).toBe(url);
105+
req.flush({});
106+
httpMock.verify();
107+
});
108+
});
109+
});
110+
111+
describe('When SSR base URL is set ', () => {
112+
describe('and it\'s in the browser', () => {
113+
beforeEach(() => {
114+
TestBed.configureTestingModule({
115+
imports: [HttpClientTestingModule],
116+
providers: [
117+
DspaceRestService,
118+
{
119+
provide: HTTP_INTERCEPTORS,
120+
useClass: DspaceRestInterceptor,
121+
multi: true,
122+
},
123+
{ provide: APP_CONFIG, useValue: appConfigWithSSR },
124+
{ provide: PLATFORM_ID, useValue: 'browser' },
125+
],
126+
});
127+
128+
httpMock = TestBed.inject(HttpTestingController);
129+
httpClient = TestBed.inject(HttpClient);
130+
});
131+
132+
it('should not modify the request', () => {
133+
const url = 'http://api.example.com/server/items';
134+
httpClient.get(url).subscribe((response) => {
135+
expect(response).toBeTruthy();
136+
});
137+
138+
const req = httpMock.expectOne(url);
139+
expect(req.request.url).toBe(url);
140+
req.flush({});
141+
httpMock.verify();
142+
});
143+
});
144+
145+
describe('and it\'s in SSR mode', () => {
146+
beforeEach(() => {
147+
TestBed.configureTestingModule({
148+
imports: [HttpClientTestingModule],
149+
providers: [
150+
DspaceRestService,
151+
{
152+
provide: HTTP_INTERCEPTORS,
153+
useClass: DspaceRestInterceptor,
154+
multi: true,
155+
},
156+
{ provide: APP_CONFIG, useValue: appConfigWithSSR },
157+
{ provide: PLATFORM_ID, useValue: 'server' },
158+
],
159+
});
160+
161+
httpMock = TestBed.inject(HttpTestingController);
162+
httpClient = TestBed.inject(HttpClient);
163+
});
164+
165+
it('should replace the base URL', () => {
166+
const url = 'http://api.example.com/server/items';
167+
const ssrBaseUrl = appConfigWithSSR.rest.ssrBaseUrl;
168+
169+
httpClient.get(url).subscribe((response) => {
170+
expect(response).toBeTruthy();
171+
});
172+
173+
const req = httpMock.expectOne(ssrBaseUrl + '/items');
174+
expect(req.request.url).toBe(ssrBaseUrl + '/items');
175+
req.flush({});
176+
httpMock.verify();
177+
});
178+
179+
it('should not replace any query param containing the base URL', () => {
180+
const url = 'http://api.example.com/server/items?url=http://api.example.com/server/item/1';
181+
const ssrBaseUrl = appConfigWithSSR.rest.ssrBaseUrl;
182+
183+
httpClient.get(url).subscribe((response) => {
184+
expect(response).toBeTruthy();
185+
});
186+
187+
const req = httpMock.expectOne(ssrBaseUrl + '/items?url=http://api.example.com/server/item/1');
188+
expect(req.request.url).toBe(ssrBaseUrl + '/items?url=http://api.example.com/server/item/1');
189+
req.flush({});
190+
httpMock.verify();
191+
});
192+
});
193+
});
194+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { isPlatformBrowser } from '@angular/common';
2+
import {
3+
HttpEvent,
4+
HttpHandler,
5+
HttpInterceptor,
6+
HttpRequest,
7+
} from '@angular/common/http';
8+
import {
9+
Inject,
10+
Injectable,
11+
PLATFORM_ID,
12+
} from '@angular/core';
13+
import { Observable } from 'rxjs';
14+
15+
import {
16+
APP_CONFIG,
17+
AppConfig,
18+
} from '../../../config/app-config.interface';
19+
import { isEmpty } from '../../shared/empty.util';
20+
21+
@Injectable()
22+
/**
23+
* This Interceptor is used to use the configured base URL for the request made during SSR execution
24+
*/
25+
export class DspaceRestInterceptor implements HttpInterceptor {
26+
27+
/**
28+
* Contains the configured application base URL
29+
* @protected
30+
*/
31+
protected baseUrl: string;
32+
protected ssrBaseUrl: string;
33+
34+
constructor(
35+
@Inject(APP_CONFIG) protected appConfig: AppConfig,
36+
@Inject(PLATFORM_ID) private platformId: string,
37+
) {
38+
this.baseUrl = this.appConfig.rest.baseUrl;
39+
this.ssrBaseUrl = this.appConfig.rest.ssrBaseUrl;
40+
}
41+
42+
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
43+
if (isPlatformBrowser(this.platformId) || isEmpty(this.ssrBaseUrl) || this.baseUrl === this.ssrBaseUrl) {
44+
return next.handle(request);
45+
}
46+
47+
// Different SSR Base URL specified so replace it in the current request url
48+
const url = request.url.replace(this.baseUrl, this.ssrBaseUrl);
49+
const newRequest: HttpRequest<any> = request.clone({ url });
50+
return next.handle(newRequest);
51+
}
52+
}

src/app/core/services/server-hard-redirect.service.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { TestBed } from '@angular/core/testing';
2+
3+
import { environment } from '../../../environments/environment.test';
24
import { ServerHardRedirectService } from './server-hard-redirect.service';
35

46
describe('ServerHardRedirectService', () => {
57

68
const mockRequest = jasmine.createSpyObj(['get']);
79
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
810

9-
const service: ServerHardRedirectService = new ServerHardRedirectService(mockRequest, mockResponse);
11+
const service: ServerHardRedirectService = new ServerHardRedirectService(environment, mockRequest, mockResponse);
1012
const origin = 'https://test-host.com:4000';
1113

1214
beforeEach(() => {

src/app/core/services/server-hard-redirect.service.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Inject, Injectable } from '@angular/core';
22
import { Request, Response } from 'express';
33
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
44
import { HardRedirectService } from './hard-redirect.service';
5+
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
6+
import { isNotEmpty } from '../../shared/empty.util';
57

68
/**
79
* Service for performing hard redirects within the server app module
@@ -10,6 +12,7 @@ import { HardRedirectService } from './hard-redirect.service';
1012
export class ServerHardRedirectService extends HardRedirectService {
1113

1214
constructor(
15+
@Inject(APP_CONFIG) protected appConfig: AppConfig,
1316
@Inject(REQUEST) protected req: Request,
1417
@Inject(RESPONSE) protected res: Response,
1518
) {
@@ -25,17 +28,22 @@ export class ServerHardRedirectService extends HardRedirectService {
2528
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
2629
*/
2730
redirect(url: string, statusCode?: number) {
28-
2931
if (url === this.req.url) {
3032
return;
3133
}
3234

35+
let redirectUrl = url;
36+
// If redirect url contains SSR base url then replace with public base url
37+
if (isNotEmpty(this.appConfig.rest.ssrBaseUrl) && this.appConfig.rest.baseUrl !== this.appConfig.rest.ssrBaseUrl) {
38+
redirectUrl = url.replace(this.appConfig.rest.ssrBaseUrl, this.appConfig.rest.baseUrl);
39+
}
40+
3341
if (this.res.finished) {
3442
const req: any = this.req;
3543
req._r_count = (req._r_count || 0) + 1;
3644

3745
console.warn('Attempted to redirect on a finished response. From',
38-
this.req.url, 'to', url);
46+
this.req.url, 'to', redirectUrl);
3947

4048
if (req._r_count > 10) {
4149
console.error('Detected a redirection loop. killing the nodejs process');
@@ -49,9 +57,9 @@ export class ServerHardRedirectService extends HardRedirectService {
4957
status = 302;
5058
}
5159

52-
console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`);
60+
console.info(`Redirecting from ${this.req.url} to ${redirectUrl} with ${status}`);
5361

54-
this.res.redirect(status, url);
62+
this.res.redirect(status, redirectUrl);
5563
this.res.end();
5664
// I haven't found a way to correctly stop Angular rendering.
5765
// So we just let it end its work, though we have already closed

0 commit comments

Comments
 (0)