Skip to content

Commit 8562ddb

Browse files
committed
[DURACOM-288] Provide a setting to use a different REST url during SSR execution
1 parent 051ee00 commit 8562ddb

10 files changed

Lines changed: 319 additions & 28 deletions

server.ts

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

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

@@ -156,7 +159,7 @@ export function app() {
156159
* Proxy the sitemaps
157160
*/
158161
router.use('/sitemap**', createProxyMiddleware({
159-
target: `${environment.rest.baseUrl}/sitemaps`,
162+
target: `${REST_BASE_URL}/sitemaps`,
160163
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
161164
changeOrigin: true,
162165
}));
@@ -165,7 +168,7 @@ export function app() {
165168
* Proxy the linksets
166169
*/
167170
router.use('/signposting**', createProxyMiddleware({
168-
target: `${environment.rest.baseUrl}`,
171+
target: `${REST_BASE_URL}`,
169172
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
170173
changeOrigin: true,
171174
}));
@@ -623,7 +626,7 @@ function start() {
623626
* The callback function to serve health check requests
624627
*/
625628
function healthCheck(req, res) {
626-
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
629+
const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
627630
axios.get(baseUrl)
628631
.then((response) => {
629632
res.status(response.status).send(response.data);

src/app/app.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
} from './app-routes';
5555
import { BROWSE_BY_DECORATOR_MAP } from './browse-by/browse-by-switcher/browse-by-decorator';
5656
import { AuthInterceptor } from './core/auth/auth.interceptor';
57+
import { DspaceRestInterceptor } from './core/dspace-rest/dspace-rest.interceptor';
5758
import { LocaleInterceptor } from './core/locale/locale.interceptor';
5859
import { LogInterceptor } from './core/log/log.interceptor';
5960
import {
@@ -148,6 +149,11 @@ export const commonAppConfig: ApplicationConfig = {
148149
useClass: LogInterceptor,
149150
multi: true,
150151
},
152+
{
153+
provide: HTTP_INTERCEPTORS,
154+
useClass: DspaceRestInterceptor,
155+
multi: true,
156+
},
151157
// register the dynamic matcher used by form. MUST be provided by the app module
152158
...DYNAMIC_MATCHER_PROVIDERS,
153159
provideCore(),
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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { TestBed } from '@angular/core/testing';
22

3+
import { environment } from '../../../environments/environment.test';
34
import { ServerHardRedirectService } from './server-hard-redirect.service';
45

56
describe('ServerHardRedirectService', () => {
67

78
const mockRequest = jasmine.createSpyObj(['get']);
89
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
910

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

1314
beforeEach(() => {

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@ import {
77
Response,
88
} from 'express';
99

10+
import {
11+
APP_CONFIG,
12+
AppConfig,
13+
} from '../../../config/app-config.interface';
1014
import {
1115
REQUEST,
1216
RESPONSE,
1317
} from '../../../express.tokens';
18+
import { isNotEmpty } from '../../shared/empty.util';
1419
import { HardRedirectService } from './hard-redirect.service';
1520

1621
/**
@@ -20,6 +25,7 @@ import { HardRedirectService } from './hard-redirect.service';
2025
export class ServerHardRedirectService extends HardRedirectService {
2126

2227
constructor(
28+
@Inject(APP_CONFIG) protected appConfig: AppConfig,
2329
@Inject(REQUEST) protected req: Request,
2430
@Inject(RESPONSE) protected res: Response,
2531
) {
@@ -35,17 +41,22 @@ export class ServerHardRedirectService extends HardRedirectService {
3541
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
3642
*/
3743
redirect(url: string, statusCode?: number) {
38-
3944
if (url === this.req.url) {
4045
return;
4146
}
4247

48+
let redirectUrl = url;
49+
// If redirect url contains SSR base url then replace with public base url
50+
if (isNotEmpty(this.appConfig.rest.ssrBaseUrl) && this.appConfig.rest.baseUrl !== this.appConfig.rest.ssrBaseUrl) {
51+
redirectUrl = url.replace(this.appConfig.rest.ssrBaseUrl, this.appConfig.rest.baseUrl);
52+
}
53+
4354
if (this.res.finished) {
4455
const req: any = this.req;
4556
req._r_count = (req._r_count || 0) + 1;
4657

4758
console.warn('Attempted to redirect on a finished response. From',
48-
this.req.url, 'to', url);
59+
this.req.url, 'to', redirectUrl);
4960

5061
if (req._r_count > 10) {
5162
console.error('Detected a redirection loop. killing the nodejs process');
@@ -59,9 +70,9 @@ export class ServerHardRedirectService extends HardRedirectService {
5970
status = 302;
6071
}
6172

62-
console.log(`Redirecting from ${this.req.url} to ${url} with ${status}`);
73+
console.info(`Redirecting from ${this.req.url} to ${redirectUrl} with ${status}`);
6374

64-
this.res.redirect(status, url);
75+
this.res.redirect(status, redirectUrl);
6576
this.res.end();
6677
// I haven't found a way to correctly stop Angular rendering.
6778
// So we just let it end its work, though we have already closed

0 commit comments

Comments
 (0)