Skip to content

Commit 4cfff33

Browse files
committed
[CST-4880] Add ServerCheckGuard in order to show internal server error page when rest server is not available
1 parent 4fdd3b8 commit 4cfff33

15 files changed

Lines changed: 290 additions & 21 deletions

src/app/app-routing-paths.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ export function getPageNotFoundRoute() {
8989
return `/${PAGE_NOT_FOUND_PATH}`;
9090
}
9191

92+
export const INTERNAL_SERVER_ERROR = '500';
93+
94+
export function getPageInternalServerErrorRoute() {
95+
return `/${INTERNAL_SERVER_ERROR}`;
96+
}
97+
9298
export const INFO_MODULE_PATH = 'info';
9399
export function getInfoModulePath() {
94100
return `/${INFO_MODULE_PATH}`;

src/app/app-routing.module.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import {
1111
FORBIDDEN_PATH,
1212
FORGOT_PASSWORD_PATH,
1313
INFO_MODULE_PATH,
14+
INTERNAL_SERVER_ERROR,
15+
LEGACY_BITSTREAM_MODULE_PATH,
1416
PROFILE_MODULE_PATH,
1517
REGISTER_PATH,
18+
REQUEST_COPY_MODULE_PATH,
1619
WORKFLOW_ITEM_MODULE_PATH,
17-
LEGACY_BITSTREAM_MODULE_PATH, REQUEST_COPY_MODULE_PATH,
1820
} from './app-routing-paths';
1921
import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routing-paths';
2022
import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths';
@@ -26,14 +28,25 @@ import { SiteRegisterGuard } from './core/data/feature-authorization/feature-aut
2628
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
2729
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
2830
import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
31+
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
32+
import { ServerCheckGuard } from './core/server-check/server-check.guard';
2933

3034
@NgModule({
3135
imports: [
32-
RouterModule.forRoot([{
33-
path: '', canActivate: [AuthBlockingGuard],
36+
RouterModule.forRoot([
37+
{ path: INTERNAL_SERVER_ERROR, component: ThemedPageInternalServerErrorComponent },
38+
{
39+
path: '',
40+
canActivate: [AuthBlockingGuard],
41+
canActivateChild: [ServerCheckGuard],
3442
children: [
3543
{ path: '', redirectTo: '/home', pathMatch: 'full' },
36-
{ path: 'reload/:rnd', component: ThemedPageNotFoundComponent, pathMatch: 'full', canActivate: [ReloadGuard] },
44+
{
45+
path: 'reload/:rnd',
46+
component: ThemedPageNotFoundComponent,
47+
pathMatch: 'full',
48+
canActivate: [ReloadGuard]
49+
},
3750
{
3851
path: 'home',
3952
loadChildren: () => import('./home-page/home-page.module')
@@ -89,7 +102,8 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
89102
.then((m) => m.ItemPageModule),
90103
canActivate: [EndUserAgreementCurrentUserGuard]
91104
},
92-
{ path: 'entities/:entity-type',
105+
{
106+
path: 'entities/:entity-type',
93107
loadChildren: () => import('./item-page/item-page.module')
94108
.then((m) => m.ItemPageModule),
95109
canActivate: [EndUserAgreementCurrentUserGuard]
@@ -133,12 +147,12 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
133147
{
134148
path: 'login',
135149
loadChildren: () => import('./login-page/login-page.module')
136-
.then((m) => m.LoginPageModule),
150+
.then((m) => m.LoginPageModule)
137151
},
138152
{
139153
path: 'logout',
140154
loadChildren: () => import('./logout-page/logout-page.module')
141-
.then((m) => m.LogoutPageModule),
155+
.then((m) => m.LogoutPageModule)
142156
},
143157
{
144158
path: 'submit',
@@ -178,7 +192,7 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
178192
},
179193
{
180194
path: INFO_MODULE_PATH,
181-
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule),
195+
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule)
182196
},
183197
{
184198
path: REQUEST_COPY_MODULE_PATH,
@@ -192,17 +206,18 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
192206
{
193207
path: 'statistics',
194208
loadChildren: () => import('./statistics-page/statistics-page-routing.module')
195-
.then((m) => m.StatisticsPageRoutingModule),
209+
.then((m) => m.StatisticsPageRoutingModule)
196210
},
197211
{
198212
path: ACCESS_CONTROL_MODULE_PATH,
199213
loadChildren: () => import('./access-control/access-control.module').then((m) => m.AccessControlModule),
200214
canActivate: [GroupAdministratorGuard],
201215
},
202216
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
203-
]}
204-
],{
205-
onSameUrlNavigation: 'reload',
217+
]
218+
}
219+
], {
220+
onSameUrlNavigation: 'reload',
206221
})
207222
],
208223
exports: [RouterModule],

src/app/app.module.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ import { ThemedFooterComponent } from './footer/themed-footer.component';
5454
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
5555
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
5656
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
57+
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
58+
import { PageInternalServerErrorComponent } from './page-internal-server-error/page-internal-server-error.component';
5759

58-
import { AppConfig, APP_CONFIG } from '../config/app-config.interface';
60+
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
5961

6062
export function getConfig() {
6163
return environment;
@@ -181,7 +183,9 @@ const DECLARATIONS = [
181183
ThemedBreadcrumbsComponent,
182184
ForbiddenComponent,
183185
ThemedForbiddenComponent,
184-
IdleModalComponent
186+
IdleModalComponent,
187+
ThemedPageInternalServerErrorComponent,
188+
PageInternalServerErrorComponent
185189
];
186190

187191
const EXPORTS = [

src/app/core/data/root-data.service.spec.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
import { RootDataService } from './root-data.service';
22
import { HALEndpointService } from '../shared/hal-endpoint.service';
33
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
4-
import { Observable } from 'rxjs';
4+
import { Observable, of } from 'rxjs';
55
import { RemoteData } from './remote-data';
66
import { Root } from './root.model';
7+
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
8+
import { cold } from 'jasmine-marbles';
79

810
describe('RootDataService', () => {
911
let service: RootDataService;
1012
let halService: HALEndpointService;
13+
let restService;
1114
let rootEndpoint;
1215

1316
beforeEach(() => {
1417
rootEndpoint = 'root-endpoint';
1518
halService = jasmine.createSpyObj('halService', {
1619
getRootHref: rootEndpoint
1720
});
18-
service = new RootDataService(null, null, null, null, halService, null, null, null);
21+
restService = jasmine.createSpyObj('halService', {
22+
get: jasmine.createSpy('get')
23+
});
24+
service = new RootDataService(null, null, null, null, halService, null, null, null, restService);
1925
(service as any).dataService = jasmine.createSpyObj('dataService', {
2026
findByHref: createSuccessfulRemoteDataObject$({})
2127
});
@@ -35,4 +41,37 @@ describe('RootDataService', () => {
3541
});
3642
});
3743
});
44+
45+
describe('checkServerAvailability', () => {
46+
let result$: Observable<boolean>;
47+
48+
it('should return observable of true when root endpoint is available', () => {
49+
const mockResponse = {
50+
statusCode: 200,
51+
statusText: 'OK'
52+
} as RawRestResponse;
53+
54+
restService.get.and.returnValue(of(mockResponse));
55+
result$ = service.checkServerAvailability();
56+
57+
expect(result$).toBeObservable(cold('(a|)', {
58+
a: true
59+
}));
60+
});
61+
62+
it('should return observable of false when root endpoint is not available', () => {
63+
const mockResponse = {
64+
statusCode: 500,
65+
statusText: 'Internal Server Error'
66+
} as RawRestResponse;
67+
68+
restService.get.and.returnValue(of(mockResponse));
69+
result$ = service.checkServerAvailability();
70+
71+
expect(result$).toBeObservable(cold('(a|)', {
72+
a: false
73+
}));
74+
});
75+
76+
});
3877
});

src/app/core/data/root-data.service.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import { RemoteData } from './remote-data';
1717
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
1818
import { FindListOptions } from './request.models';
1919
import { PaginatedList } from './paginated-list.model';
20+
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
21+
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
22+
import { catchError, map } from 'rxjs/operators';
23+
import { of } from 'rxjs/internal/observable/of';
2024

2125
/* tslint:disable:max-classes-per-file */
2226

@@ -59,10 +63,24 @@ export class RootDataService {
5963
protected halService: HALEndpointService,
6064
protected notificationsService: NotificationsService,
6165
protected http: HttpClient,
62-
protected comparator: DefaultChangeAnalyzer<Root>) {
66+
protected comparator: DefaultChangeAnalyzer<Root>,
67+
protected restService: DspaceRestService) {
6368
this.dataService = new DataServiceImpl(requestService, rdbService, null, objectCache, halService, notificationsService, http, comparator);
6469
}
6570

71+
/**
72+
* Check if root endpoint is available
73+
*/
74+
checkServerAvailability(): Observable<boolean> {
75+
return this.restService.get(this.halService.getRootHref()).pipe(
76+
catchError((err ) => {
77+
console.error(err);
78+
return of(false);
79+
}),
80+
map((res: RawRestResponse) => res.statusCode === 200)
81+
);
82+
}
83+
6684
/**
6785
* Find the {@link Root} object of the REST API
6886
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
@@ -106,5 +124,12 @@ export class RootDataService {
106124
findAllByHref(href: string | Observable<string>, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Root>[]): Observable<RemoteData<PaginatedList<Root>>> {
107125
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
108126
}
127+
128+
/**
129+
* Set to sale the root endpoint cache hit
130+
*/
131+
invalidateRootCache() {
132+
this.requestService.setStaleByHrefSubstring('server/api');
133+
}
109134
}
110135
/* tslint:enable:max-classes-per-file */
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { ServerCheckGuard } from './server-check.guard';
2+
import { Router } from '@angular/router';
3+
4+
import { of } from 'rxjs';
5+
import { take } from 'rxjs/operators';
6+
7+
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
8+
import { RootDataService } from '../data/root-data.service';
9+
import SpyObj = jasmine.SpyObj;
10+
11+
describe('ServerCheckGuard', () => {
12+
let guard: ServerCheckGuard;
13+
let router: SpyObj<Router>;
14+
let rootDataServiceStub: SpyObj<RootDataService>;
15+
16+
rootDataServiceStub = jasmine.createSpyObj('RootDataService', {
17+
checkServerAvailability: jasmine.createSpy('checkServerAvailability'),
18+
invalidateRootCache: jasmine.createSpy('invalidateRootCache')
19+
});
20+
router = jasmine.createSpyObj('Router', {
21+
navigateByUrl: jasmine.createSpy('navigateByUrl')
22+
});
23+
24+
beforeEach(() => {
25+
guard = new ServerCheckGuard(router, rootDataServiceStub);
26+
});
27+
28+
afterEach(() => {
29+
router.navigateByUrl.calls.reset();
30+
rootDataServiceStub.invalidateRootCache.calls.reset();
31+
});
32+
33+
it('should be created', () => {
34+
expect(guard).toBeTruthy();
35+
});
36+
37+
describe('when root endpoint has succeeded', () => {
38+
beforeEach(() => {
39+
rootDataServiceStub.checkServerAvailability.and.returnValue(of(true));
40+
});
41+
42+
it('should not redirect to error page', () => {
43+
guard.canActivateChild({} as any, {} as any).pipe(
44+
take(1)
45+
).subscribe((canActivate: boolean) => {
46+
expect(canActivate).toEqual(true);
47+
expect(rootDataServiceStub.invalidateRootCache).not.toHaveBeenCalled();
48+
expect(router.navigateByUrl).not.toHaveBeenCalled();
49+
});
50+
});
51+
});
52+
53+
describe('when root endpoint has not succeeded', () => {
54+
beforeEach(() => {
55+
rootDataServiceStub.checkServerAvailability.and.returnValue(of(false));
56+
});
57+
58+
it('should redirect to error page', () => {
59+
guard.canActivateChild({} as any, {} as any).pipe(
60+
take(1)
61+
).subscribe((canActivate: boolean) => {
62+
expect(canActivate).toEqual(false);
63+
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalled();
64+
expect(router.navigateByUrl).toHaveBeenCalledWith(getPageInternalServerErrorRoute());
65+
});
66+
});
67+
});
68+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Injectable } from '@angular/core';
2+
import { ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
3+
4+
import { Observable } from 'rxjs';
5+
import { take, tap } from 'rxjs/operators';
6+
7+
import { RootDataService } from '../data/root-data.service';
8+
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
9+
10+
@Injectable({
11+
providedIn: 'root'
12+
})
13+
/**
14+
* A guard that checks if root api endpoint is reachable.
15+
* If not redirect to 500 error page
16+
*/
17+
export class ServerCheckGuard implements CanActivateChild {
18+
constructor(private router: Router, private rootDataService: RootDataService) {
19+
}
20+
21+
/**
22+
* True when root api endpoint is reachable.
23+
*/
24+
canActivateChild(
25+
route: ActivatedRouteSnapshot,
26+
state: RouterStateSnapshot): Observable<boolean> {
27+
28+
return this.rootDataService.checkServerAvailability().pipe(
29+
take(1),
30+
tap((isAvailable: boolean) => {
31+
if (!isAvailable) {
32+
this.rootDataService.invalidateRootCache();
33+
this.router.navigateByUrl(getPageInternalServerErrorRoute());
34+
}
35+
})
36+
);
37+
38+
}
39+
}

src/app/core/services/server-response.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,8 @@ export class ServerResponseService {
3131
setNotFound(message = 'Not found'): this {
3232
return this.setStatus(404, message);
3333
}
34+
35+
setInternalServerError(message = 'Internal Server Error'): this {
36+
return this.setStatus(500, message);
37+
}
3438
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<div class="page-internal-server-error container">
2+
<h1>500</h1>
3+
<h2><small>{{"500.page-internal-server-error" | translate}}</small></h2>
4+
<br/>
5+
<p>{{"500.help" | translate}}</p>
6+
<br/>
7+
<p class="text-center">
8+
<a href="/home" class="btn btn-primary">{{"500.link.home-page" | translate}}</a>
9+
</p>
10+
</div>

src/app/page-internal-server-error/page-internal-server-error.component.scss

Whitespace-only changes.

0 commit comments

Comments
 (0)