mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 02:24:11 +00:00
Merge pull request #1407 from 4Science/DSC-287-Show-an-error-page-if-the-REST-API-is-not-available
Show an error page if the rest api is not available
This commit is contained in:
@@ -89,6 +89,12 @@ export function getPageNotFoundRoute() {
|
|||||||
return `/${PAGE_NOT_FOUND_PATH}`;
|
return `/${PAGE_NOT_FOUND_PATH}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const INTERNAL_SERVER_ERROR = '500';
|
||||||
|
|
||||||
|
export function getPageInternalServerErrorRoute() {
|
||||||
|
return `/${INTERNAL_SERVER_ERROR}`;
|
||||||
|
}
|
||||||
|
|
||||||
export const INFO_MODULE_PATH = 'info';
|
export const INFO_MODULE_PATH = 'info';
|
||||||
export function getInfoModulePath() {
|
export function getInfoModulePath() {
|
||||||
return `/${INFO_MODULE_PATH}`;
|
return `/${INFO_MODULE_PATH}`;
|
||||||
|
@@ -11,10 +11,12 @@ import {
|
|||||||
FORBIDDEN_PATH,
|
FORBIDDEN_PATH,
|
||||||
FORGOT_PASSWORD_PATH,
|
FORGOT_PASSWORD_PATH,
|
||||||
INFO_MODULE_PATH,
|
INFO_MODULE_PATH,
|
||||||
|
INTERNAL_SERVER_ERROR,
|
||||||
|
LEGACY_BITSTREAM_MODULE_PATH,
|
||||||
PROFILE_MODULE_PATH,
|
PROFILE_MODULE_PATH,
|
||||||
REGISTER_PATH,
|
REGISTER_PATH,
|
||||||
|
REQUEST_COPY_MODULE_PATH,
|
||||||
WORKFLOW_ITEM_MODULE_PATH,
|
WORKFLOW_ITEM_MODULE_PATH,
|
||||||
LEGACY_BITSTREAM_MODULE_PATH, REQUEST_COPY_MODULE_PATH,
|
|
||||||
} from './app-routing-paths';
|
} from './app-routing-paths';
|
||||||
import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routing-paths';
|
import { COLLECTION_MODULE_PATH } from './collection-page/collection-page-routing-paths';
|
||||||
import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths';
|
import { COMMUNITY_MODULE_PATH } from './community-page/community-page-routing-paths';
|
||||||
@@ -26,14 +28,25 @@ import { SiteRegisterGuard } from './core/data/feature-authorization/feature-aut
|
|||||||
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
|
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
|
||||||
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
|
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
|
||||||
import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
import { GroupAdministratorGuard } from './core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
||||||
|
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||||
|
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
RouterModule.forRoot([{
|
RouterModule.forRoot([
|
||||||
path: '', canActivate: [AuthBlockingGuard],
|
{ path: INTERNAL_SERVER_ERROR, component: ThemedPageInternalServerErrorComponent },
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
canActivate: [AuthBlockingGuard],
|
||||||
|
canActivateChild: [ServerCheckGuard],
|
||||||
children: [
|
children: [
|
||||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||||
{ path: 'reload/:rnd', component: ThemedPageNotFoundComponent, pathMatch: 'full', canActivate: [ReloadGuard] },
|
{
|
||||||
|
path: 'reload/:rnd',
|
||||||
|
component: ThemedPageNotFoundComponent,
|
||||||
|
pathMatch: 'full',
|
||||||
|
canActivate: [ReloadGuard]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'home',
|
path: 'home',
|
||||||
loadChildren: () => import('./home-page/home-page.module')
|
loadChildren: () => import('./home-page/home-page.module')
|
||||||
@@ -89,7 +102,8 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
|||||||
.then((m) => m.ItemPageModule),
|
.then((m) => m.ItemPageModule),
|
||||||
canActivate: [EndUserAgreementCurrentUserGuard]
|
canActivate: [EndUserAgreementCurrentUserGuard]
|
||||||
},
|
},
|
||||||
{ path: 'entities/:entity-type',
|
{
|
||||||
|
path: 'entities/:entity-type',
|
||||||
loadChildren: () => import('./item-page/item-page.module')
|
loadChildren: () => import('./item-page/item-page.module')
|
||||||
.then((m) => m.ItemPageModule),
|
.then((m) => m.ItemPageModule),
|
||||||
canActivate: [EndUserAgreementCurrentUserGuard]
|
canActivate: [EndUserAgreementCurrentUserGuard]
|
||||||
@@ -133,12 +147,12 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
|||||||
{
|
{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
loadChildren: () => import('./login-page/login-page.module')
|
loadChildren: () => import('./login-page/login-page.module')
|
||||||
.then((m) => m.LoginPageModule),
|
.then((m) => m.LoginPageModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'logout',
|
path: 'logout',
|
||||||
loadChildren: () => import('./logout-page/logout-page.module')
|
loadChildren: () => import('./logout-page/logout-page.module')
|
||||||
.then((m) => m.LogoutPageModule),
|
.then((m) => m.LogoutPageModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'submit',
|
path: 'submit',
|
||||||
@@ -178,7 +192,7 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: INFO_MODULE_PATH,
|
path: INFO_MODULE_PATH,
|
||||||
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule),
|
loadChildren: () => import('./info/info.module').then((m) => m.InfoModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: REQUEST_COPY_MODULE_PATH,
|
path: REQUEST_COPY_MODULE_PATH,
|
||||||
@@ -192,7 +206,7 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
|||||||
{
|
{
|
||||||
path: 'statistics',
|
path: 'statistics',
|
||||||
loadChildren: () => import('./statistics-page/statistics-page-routing.module')
|
loadChildren: () => import('./statistics-page/statistics-page-routing.module')
|
||||||
.then((m) => m.StatisticsPageRoutingModule),
|
.then((m) => m.StatisticsPageRoutingModule)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ACCESS_CONTROL_MODULE_PATH,
|
path: ACCESS_CONTROL_MODULE_PATH,
|
||||||
@@ -200,9 +214,10 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
|||||||
canActivate: [GroupAdministratorGuard],
|
canActivate: [GroupAdministratorGuard],
|
||||||
},
|
},
|
||||||
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
|
{ path: '**', pathMatch: 'full', component: ThemedPageNotFoundComponent },
|
||||||
]}
|
]
|
||||||
],{
|
}
|
||||||
onSameUrlNavigation: 'reload',
|
], {
|
||||||
|
onSameUrlNavigation: 'reload',
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
|
@@ -54,6 +54,8 @@ import { ThemedFooterComponent } from './footer/themed-footer.component';
|
|||||||
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
|
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
|
||||||
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
|
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
|
||||||
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||||
|
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
|
||||||
|
import { PageInternalServerErrorComponent } from './page-internal-server-error/page-internal-server-error.component';
|
||||||
|
|
||||||
import { AppConfig, APP_CONFIG } from '../config/app-config.interface';
|
import { AppConfig, APP_CONFIG } from '../config/app-config.interface';
|
||||||
|
|
||||||
@@ -181,7 +183,9 @@ const DECLARATIONS = [
|
|||||||
ThemedBreadcrumbsComponent,
|
ThemedBreadcrumbsComponent,
|
||||||
ForbiddenComponent,
|
ForbiddenComponent,
|
||||||
ThemedForbiddenComponent,
|
ThemedForbiddenComponent,
|
||||||
IdleModalComponent
|
IdleModalComponent,
|
||||||
|
ThemedPageInternalServerErrorComponent,
|
||||||
|
PageInternalServerErrorComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const EXPORTS = [
|
const EXPORTS = [
|
||||||
|
@@ -106,5 +106,12 @@ export class RootDataService {
|
|||||||
findAllByHref(href: string | Observable<string>, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Root>[]): Observable<RemoteData<PaginatedList<Root>>> {
|
findAllByHref(href: string | Observable<string>, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Root>[]): Observable<RemoteData<PaginatedList<Root>>> {
|
||||||
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
return this.dataService.findAllByHref(href, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to sale the root endpoint cache hit
|
||||||
|
*/
|
||||||
|
invalidateRootCache() {
|
||||||
|
this.requestService.setStaleByHrefSubstring('server/api');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
68
src/app/core/server-check/server-check.guard.spec.ts
Normal file
68
src/app/core/server-check/server-check.guard.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { ServerCheckGuard } from './server-check.guard';
|
||||||
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { RootDataService } from '../data/root-data.service';
|
||||||
|
import { Root } from '../data/root.model';
|
||||||
|
import SpyObj = jasmine.SpyObj;
|
||||||
|
|
||||||
|
describe('ServerCheckGuard', () => {
|
||||||
|
let guard: ServerCheckGuard;
|
||||||
|
let router: SpyObj<Router>;
|
||||||
|
let rootDataServiceStub: SpyObj<RootDataService>;
|
||||||
|
|
||||||
|
rootDataServiceStub = jasmine.createSpyObj('RootDataService', {
|
||||||
|
findRoot: jasmine.createSpy('findRoot'),
|
||||||
|
invalidateRootCache: jasmine.createSpy('invalidateRootCache')
|
||||||
|
});
|
||||||
|
router = jasmine.createSpyObj('Router', {
|
||||||
|
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
guard = new ServerCheckGuard(router, rootDataServiceStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
router.navigateByUrl.calls.reset();
|
||||||
|
rootDataServiceStub.invalidateRootCache.calls.reset();
|
||||||
|
rootDataServiceStub.findRoot.calls.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(guard).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when root endpoint has succeeded', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
rootDataServiceStub.findRoot.and.returnValue(createSuccessfulRemoteDataObject$<Root>({} as any));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not redirect to error page', () => {
|
||||||
|
guard.canActivateChild({} as any, {} as any).pipe(
|
||||||
|
take(1)
|
||||||
|
).subscribe((canActivate: boolean) => {
|
||||||
|
expect(canActivate).toEqual(true);
|
||||||
|
expect(rootDataServiceStub.invalidateRootCache).not.toHaveBeenCalled();
|
||||||
|
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when root endpoint has not succeeded', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
rootDataServiceStub.findRoot.and.returnValue(createFailedRemoteDataObject$<Root>());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should redirect to error page', () => {
|
||||||
|
guard.canActivateChild({} as any, {} as any).pipe(
|
||||||
|
take(1)
|
||||||
|
).subscribe((canActivate: boolean) => {
|
||||||
|
expect(canActivate).toEqual(false);
|
||||||
|
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalled();
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith(getPageInternalServerErrorRoute());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
46
src/app/core/server-check/server-check.guard.ts
Normal file
46
src/app/core/server-check/server-check.guard.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
|
||||||
|
|
||||||
|
import { interval, Observable, race } from 'rxjs';
|
||||||
|
import { map, mergeMapTo, tap } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { RootDataService } from '../data/root-data.service';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
|
||||||
|
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* A guard that checks if root api endpoint is reachable.
|
||||||
|
* If not redirect to 500 error page
|
||||||
|
*/
|
||||||
|
export class ServerCheckGuard implements CanActivateChild {
|
||||||
|
constructor(private router: Router, private rootDataService: RootDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when root api endpoint is reachable.
|
||||||
|
*/
|
||||||
|
canActivateChild(
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot): Observable<boolean> {
|
||||||
|
|
||||||
|
const uncachedCheck$ = this.rootDataService.findRoot(false);
|
||||||
|
// fallback observable used if the uncached one hangs and doesn't emit value
|
||||||
|
const cachedCheck$ = interval(200).pipe(mergeMapTo((this.rootDataService.findRoot())));
|
||||||
|
|
||||||
|
return race([uncachedCheck$, cachedCheck$]).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((res: RemoteData<any>) => res.hasSucceeded),
|
||||||
|
tap((hasSucceeded: boolean) => {
|
||||||
|
if (!hasSucceeded) {
|
||||||
|
this.rootDataService.invalidateRootCache();
|
||||||
|
this.router.navigateByUrl(getPageInternalServerErrorRoute());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -31,4 +31,8 @@ export class ServerResponseService {
|
|||||||
setNotFound(message = 'Not found'): this {
|
setNotFound(message = 'Not found'): this {
|
||||||
return this.setStatus(404, message);
|
return this.setStatus(404, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setInternalServerError(message = 'Internal Server Error'): this {
|
||||||
|
return this.setStatus(500, message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="page-internal-server-error container">
|
||||||
|
<h1>500</h1>
|
||||||
|
<h2><small>{{"500.page-internal-server-error" | translate}}</small></h2>
|
||||||
|
<br/>
|
||||||
|
<p>{{"500.help" | translate}}</p>
|
||||||
|
<br/>
|
||||||
|
<p class="text-center">
|
||||||
|
<a routerLink="/home" class="btn btn-primary">{{"500.link.home-page" | translate}}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
@@ -0,0 +1,23 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||||
|
import { ServerResponseService } from '../core/services/server-response.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component representing the `PageInternalServer` DSpace page.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-page-internal-server-error',
|
||||||
|
styleUrls: ['./page-internal-server-error.component.scss'],
|
||||||
|
templateUrl: './page-internal-server-error.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Default
|
||||||
|
})
|
||||||
|
export class PageInternalServerErrorComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize instance variables
|
||||||
|
*
|
||||||
|
* @param {ServerResponseService} responseService
|
||||||
|
*/
|
||||||
|
constructor(private responseService: ServerResponseService) {
|
||||||
|
this.responseService.setInternalServerError();
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../shared/theme-support/themed.component';
|
||||||
|
import { PageInternalServerErrorComponent } from './page-internal-server-error.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for PageInternalServerErrorComponent
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-search-page',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
export class ThemedPageInternalServerErrorComponent extends ThemedComponent<PageInternalServerErrorComponent> {
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'PageInternalServerErrorComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../themes/${themeName}/app/page-internal-server-error/page-internal-server-error.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import(`./page-internal-server-error.component`);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { Component, Inject, OnInit, Input } from '@angular/core';
|
import { Component, Inject, Input, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs';
|
import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs';
|
||||||
@@ -19,6 +19,7 @@ import { ThemeConfig } from '../../config/theme.model';
|
|||||||
import { Angulartics2DSpace } from '../statistics/angulartics/dspace-provider';
|
import { Angulartics2DSpace } from '../statistics/angulartics/dspace-provider';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { slideSidebarPadding } from '../shared/animations/slide';
|
import { slideSidebarPadding } from '../shared/animations/slide';
|
||||||
|
import { getPageInternalServerErrorRoute } from '../app-routing-paths';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-root',
|
selector: 'ds-root',
|
||||||
@@ -68,9 +69,13 @@ export class RootComponent implements OnInit {
|
|||||||
this.totalSidebarWidth = this.cssService.getVariable('totalSidebarWidth');
|
this.totalSidebarWidth = this.cssService.getVariable('totalSidebarWidth');
|
||||||
|
|
||||||
const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN);
|
const sidebarCollapsed = this.menuService.isMenuCollapsed(MenuID.ADMIN);
|
||||||
this.slideSidebarOver = combineLatestObservable(sidebarCollapsed, this.windowService.isXsOrSm())
|
this.slideSidebarOver = combineLatestObservable([sidebarCollapsed, this.windowService.isXsOrSm()])
|
||||||
.pipe(
|
.pipe(
|
||||||
map(([collapsed, mobile]) => collapsed || mobile)
|
map(([collapsed, mobile]) => collapsed || mobile)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.router.url === getPageInternalServerErrorRoute()) {
|
||||||
|
this.shouldShowRouteLoader = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,11 @@
|
|||||||
|
|
||||||
"403.forbidden": "forbidden",
|
"403.forbidden": "forbidden",
|
||||||
|
|
||||||
|
"500.page-internal-server-error": "Service Unavailable",
|
||||||
|
|
||||||
|
"500.help": "The server is temporarily unable to service your request due to maintenance downtime or capacity problems. Please try again later.",
|
||||||
|
|
||||||
|
"500.link.home-page": "Take me to the home page",
|
||||||
|
|
||||||
|
|
||||||
"404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ",
|
"404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ",
|
||||||
|
Reference in New Issue
Block a user