|
|
|
@ -1,132 +1,45 @@ |
|
|
|
import { |
|
|
|
AuthService, |
|
|
|
HttpErrorReporterService, |
|
|
|
LocalizationParam, |
|
|
|
RouterEvents, |
|
|
|
SessionStateService, |
|
|
|
} from '@abp/ng.core'; |
|
|
|
import { HttpErrorReporterService } from '@abp/ng.core'; |
|
|
|
import { HttpErrorResponse } from '@angular/common/http'; |
|
|
|
import { |
|
|
|
ApplicationRef, |
|
|
|
ComponentFactoryResolver, |
|
|
|
ComponentRef, |
|
|
|
EmbeddedViewRef, |
|
|
|
Injectable, |
|
|
|
Injector, |
|
|
|
RendererFactory2, |
|
|
|
} from '@angular/core'; |
|
|
|
import { NavigationError, ResolveEnd } from '@angular/router'; |
|
|
|
import { Observable, of, Subject, throwError } from 'rxjs'; |
|
|
|
import { inject, Injectable, Injector } from '@angular/core'; |
|
|
|
import { Observable, of, throwError } from 'rxjs'; |
|
|
|
import { catchError, filter, switchMap } from 'rxjs/operators'; |
|
|
|
import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; |
|
|
|
import { ErrorScreenErrorCodes, HttpErrorConfig } from '../models/common'; |
|
|
|
import { CustomHttpErrorHandlerService } from '../models/common'; |
|
|
|
import { Confirmation } from '../models/confirmation'; |
|
|
|
import { ConfirmationService } from '../services/confirmation.service'; |
|
|
|
import { HTTP_ERROR_HANDLER } from '../tokens/http-error.token'; |
|
|
|
|
|
|
|
export const DEFAULT_ERROR_MESSAGES = { |
|
|
|
defaultError: { |
|
|
|
title: 'An error has occurred!', |
|
|
|
details: 'Error detail not sent by server.', |
|
|
|
}, |
|
|
|
defaultError401: { |
|
|
|
title: 'You are not authenticated!', |
|
|
|
details: 'You should be authenticated (sign in) in order to perform this operation.', |
|
|
|
}, |
|
|
|
defaultError403: { |
|
|
|
title: 'You are not authorized!', |
|
|
|
details: 'You are not allowed to perform this operation.', |
|
|
|
}, |
|
|
|
defaultError404: { |
|
|
|
title: 'Resource not found!', |
|
|
|
details: 'The resource requested could not found on the server.', |
|
|
|
}, |
|
|
|
defaultError500: { |
|
|
|
title: 'Internal server error', |
|
|
|
details: 'Error detail not sent by server.', |
|
|
|
}, |
|
|
|
}; |
|
|
|
|
|
|
|
export const DEFAULT_ERROR_LOCALIZATIONS = { |
|
|
|
defaultError: { |
|
|
|
title: 'AbpUi::DefaultErrorMessage', |
|
|
|
details: 'AbpUi::DefaultErrorMessageDetail', |
|
|
|
}, |
|
|
|
defaultError401: { |
|
|
|
title: 'AbpUi::DefaultErrorMessage401', |
|
|
|
details: 'AbpUi::DefaultErrorMessage401Detail', |
|
|
|
}, |
|
|
|
defaultError403: { |
|
|
|
title: 'AbpUi::DefaultErrorMessage403', |
|
|
|
details: 'AbpUi::DefaultErrorMessage403Detail', |
|
|
|
}, |
|
|
|
defaultError404: { |
|
|
|
title: 'AbpUi::DefaultErrorMessage404', |
|
|
|
details: 'AbpUi::DefaultErrorMessage404Detail', |
|
|
|
}, |
|
|
|
defaultError500: { |
|
|
|
title: 'AbpUi::500Message', |
|
|
|
details: 'AbpUi::DefaultErrorMessage', |
|
|
|
}, |
|
|
|
}; |
|
|
|
import { CUSTOM_ERROR_HANDLERS, HTTP_ERROR_HANDLER } from '../tokens/http-error.token'; |
|
|
|
import { DEFAULT_ERROR_LOCALIZATIONS, DEFAULT_ERROR_MESSAGES } from '../constants/default-errors'; |
|
|
|
import { RouterErrorHandlerService } from '../services/router-error-handler.service'; |
|
|
|
import { HTTP_ERROR_CONFIG } from '../tokens/http-error.token'; |
|
|
|
|
|
|
|
@Injectable({ providedIn: 'root' }) |
|
|
|
export class ErrorHandler { |
|
|
|
componentRef: ComponentRef<HttpErrorWrapperComponent> | null = null; |
|
|
|
|
|
|
|
protected httpErrorHandler = this.injector.get(HTTP_ERROR_HANDLER, (_, err: HttpErrorResponse) => |
|
|
|
throwError(err), |
|
|
|
); |
|
|
|
|
|
|
|
protected httpErrorReporter: HttpErrorReporterService; |
|
|
|
protected routerEvents: RouterEvents; |
|
|
|
protected confirmationService: ConfirmationService; |
|
|
|
protected cfRes: ComponentFactoryResolver; |
|
|
|
protected rendererFactory: RendererFactory2; |
|
|
|
protected httpErrorConfig: HttpErrorConfig; |
|
|
|
protected sessionStateService: SessionStateService; |
|
|
|
private authService: AuthService; |
|
|
|
|
|
|
|
private httpErrorReporter = inject(HttpErrorReporterService); |
|
|
|
private confirmationService = inject(ConfirmationService); |
|
|
|
private routerErrorHandlerService = inject(RouterErrorHandlerService); |
|
|
|
protected httpErrorConfig = inject(HTTP_ERROR_CONFIG); |
|
|
|
private customErrorHandlers = inject(CUSTOM_ERROR_HANDLERS); |
|
|
|
private defaultHttpErrorHandler = (_, err: HttpErrorResponse) => throwError(() => err); |
|
|
|
private httpErrorHandler = |
|
|
|
inject(HTTP_ERROR_HANDLER, { optional: true }) || this.defaultHttpErrorHandler; |
|
|
|
|
|
|
|
constructor(protected injector: Injector) { |
|
|
|
this.httpErrorReporter = injector.get(HttpErrorReporterService); |
|
|
|
this.routerEvents = injector.get(RouterEvents); |
|
|
|
this.confirmationService = injector.get(ConfirmationService); |
|
|
|
this.cfRes = injector.get(ComponentFactoryResolver); |
|
|
|
this.rendererFactory = injector.get(RendererFactory2); |
|
|
|
this.httpErrorConfig = injector.get('HTTP_ERROR_CONFIG'); |
|
|
|
this.authService = this.injector.get(AuthService); |
|
|
|
this.sessionStateService = this.injector.get(SessionStateService); |
|
|
|
|
|
|
|
this.listenToRestError(); |
|
|
|
this.listenToRouterError(); |
|
|
|
this.listenToRouterDataResolved(); |
|
|
|
} |
|
|
|
|
|
|
|
protected listenToRouterError() { |
|
|
|
this.routerEvents |
|
|
|
.getNavigationEvents('Error') |
|
|
|
.pipe(filter(this.filterRouteErrors)) |
|
|
|
.subscribe(() => this.show404Page()); |
|
|
|
} |
|
|
|
|
|
|
|
protected listenToRouterDataResolved() { |
|
|
|
this.routerEvents |
|
|
|
.getEvents(ResolveEnd) |
|
|
|
.pipe(filter(() => !!this.componentRef)) |
|
|
|
.subscribe(() => { |
|
|
|
this.componentRef?.destroy(); |
|
|
|
this.componentRef = null; |
|
|
|
}); |
|
|
|
this.routerErrorHandlerService.listen(); |
|
|
|
} |
|
|
|
|
|
|
|
protected listenToRestError() { |
|
|
|
this.httpErrorReporter.reporter$ |
|
|
|
.pipe(filter(this.filterRestErrors), switchMap(this.executeErrorHandler)) |
|
|
|
.subscribe(); |
|
|
|
.subscribe(err => { |
|
|
|
this.handleError(err); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
private executeErrorHandler = (error: any) => { |
|
|
|
private executeErrorHandler = (error: HttpErrorResponse) => { |
|
|
|
const errHandler = this.httpErrorHandler(this.injector, error); |
|
|
|
const isObservable = errHandler instanceof Observable; |
|
|
|
const response = isObservable ? errHandler : of(null); |
|
|
|
@ -139,212 +52,41 @@ export class ErrorHandler { |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
private handleError(err: any) { |
|
|
|
const body = err?.error?.error || { |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, |
|
|
|
}; |
|
|
|
|
|
|
|
if (err instanceof HttpErrorResponse && err.headers.get('Abp-Tenant-Resolve-Error')) { |
|
|
|
this.sessionStateService.setTenant(null) |
|
|
|
this.authService.logout().subscribe(); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (err instanceof HttpErrorResponse && err.headers.get('_AbpErrorFormat')) { |
|
|
|
const confirmation$ = this.showErrorWithRequestBody(body); |
|
|
|
protected sortHttpErrorHandlers( |
|
|
|
a: CustomHttpErrorHandlerService, |
|
|
|
b: CustomHttpErrorHandlerService, |
|
|
|
) { |
|
|
|
return (b.priority || 0) - (a.priority || 0); |
|
|
|
} |
|
|
|
|
|
|
|
if (err.status === 401) { |
|
|
|
confirmation$.subscribe(() => { |
|
|
|
this.navigateToLogin(); |
|
|
|
}); |
|
|
|
} |
|
|
|
} else { |
|
|
|
switch (err.status) { |
|
|
|
case 401: |
|
|
|
this.canCreateCustomError(401) |
|
|
|
? this.show401Page() |
|
|
|
: this.showError( |
|
|
|
{ |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError401.title, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.title, |
|
|
|
}, |
|
|
|
{ |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError401.details, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.details, |
|
|
|
}, |
|
|
|
).subscribe(() => this.navigateToLogin()); |
|
|
|
break; |
|
|
|
case 403: |
|
|
|
this.createErrorComponent({ |
|
|
|
title: { |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError403.title, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.title, |
|
|
|
}, |
|
|
|
details: { |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError403.details, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError403.details, |
|
|
|
}, |
|
|
|
status: 403, |
|
|
|
}); |
|
|
|
break; |
|
|
|
case 404: |
|
|
|
this.canCreateCustomError(404) |
|
|
|
? this.show404Page() |
|
|
|
: this.showError( |
|
|
|
{ |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.details, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.details, |
|
|
|
}, |
|
|
|
{ |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.title, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title, |
|
|
|
}, |
|
|
|
); |
|
|
|
break; |
|
|
|
case 500: |
|
|
|
this.createErrorComponent({ |
|
|
|
title: { |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError500.title, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.title, |
|
|
|
}, |
|
|
|
details: { |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError500.details, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.details, |
|
|
|
}, |
|
|
|
status: 500, |
|
|
|
}); |
|
|
|
break; |
|
|
|
case 0: |
|
|
|
if (err.statusText === 'Unknown Error') { |
|
|
|
this.createErrorComponent({ |
|
|
|
title: { |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, |
|
|
|
}, |
|
|
|
details: err.message, |
|
|
|
isHomeShow: false, |
|
|
|
}); |
|
|
|
} |
|
|
|
break; |
|
|
|
default: |
|
|
|
this.showError( |
|
|
|
{ |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.details, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.details, |
|
|
|
}, |
|
|
|
{ |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, |
|
|
|
}, |
|
|
|
); |
|
|
|
break; |
|
|
|
private handleError(err: unknown) { |
|
|
|
if (this.customErrorHandlers && this.customErrorHandlers.length) { |
|
|
|
const canHandleService = this.customErrorHandlers |
|
|
|
.sort(this.sortHttpErrorHandlers) |
|
|
|
.find(service => service.canHandle(err)); |
|
|
|
if (canHandleService) { |
|
|
|
canHandleService.execute(); |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
this.showError().subscribe(); |
|
|
|
} |
|
|
|
|
|
|
|
protected show401Page() { |
|
|
|
this.createErrorComponent({ |
|
|
|
title: { |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError401.title, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.title, |
|
|
|
}, |
|
|
|
status: 401, |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
protected show404Page() { |
|
|
|
this.createErrorComponent({ |
|
|
|
title: { |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.title, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title, |
|
|
|
}, |
|
|
|
status: 404, |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
protected showErrorWithRequestBody(body: any) { |
|
|
|
let message: LocalizationParam; |
|
|
|
let title: LocalizationParam; |
|
|
|
|
|
|
|
if (body.details) { |
|
|
|
message = body.details; |
|
|
|
title = body.message; |
|
|
|
} else if (body.message) { |
|
|
|
title = { |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, |
|
|
|
}; |
|
|
|
message = body.message; |
|
|
|
} else { |
|
|
|
message = body.message || { |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, |
|
|
|
}; |
|
|
|
title = ''; |
|
|
|
} |
|
|
|
|
|
|
|
return this.showError(message, title); |
|
|
|
} |
|
|
|
|
|
|
|
protected showError( |
|
|
|
message: LocalizationParam, |
|
|
|
title: LocalizationParam, |
|
|
|
): Observable<Confirmation.Status> { |
|
|
|
protected showError(): Observable<Confirmation.Status> { |
|
|
|
const title = { |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, |
|
|
|
}; |
|
|
|
const message = { |
|
|
|
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.details, |
|
|
|
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.details, |
|
|
|
}; |
|
|
|
return this.confirmationService.error(message, title, { |
|
|
|
hideCancelBtn: true, |
|
|
|
yesText: 'AbpAccount::Close', |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
private navigateToLogin() { |
|
|
|
this.authService.navigateToLogin(); |
|
|
|
} |
|
|
|
|
|
|
|
createErrorComponent(instance: Partial<HttpErrorWrapperComponent>) { |
|
|
|
const renderer = this.rendererFactory.createRenderer(null, null); |
|
|
|
const host = renderer.selectRootElement(document.body, true); |
|
|
|
|
|
|
|
this.componentRef = this.cfRes |
|
|
|
.resolveComponentFactory(HttpErrorWrapperComponent) |
|
|
|
.create(this.injector); |
|
|
|
|
|
|
|
for (const key in instance) { |
|
|
|
/* istanbul ignore else */ |
|
|
|
if (Object.prototype.hasOwnProperty.call(this.componentRef.instance, key)) { |
|
|
|
(this.componentRef.instance as any)[key] = (instance as any)[key]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
this.componentRef.instance.hideCloseIcon = !!this.httpErrorConfig.errorScreen?.hideCloseIcon; |
|
|
|
const appRef = this.injector.get(ApplicationRef); |
|
|
|
|
|
|
|
if (this.canCreateCustomError(instance.status as ErrorScreenErrorCodes)) { |
|
|
|
this.componentRef.instance.cfRes = this.cfRes; |
|
|
|
this.componentRef.instance.appRef = appRef; |
|
|
|
this.componentRef.instance.injector = this.injector; |
|
|
|
this.componentRef.instance.customComponent = this.httpErrorConfig.errorScreen?.component; |
|
|
|
} |
|
|
|
|
|
|
|
appRef.attachView(this.componentRef.hostView); |
|
|
|
renderer.appendChild(host, (this.componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0]); |
|
|
|
|
|
|
|
const destroy$ = new Subject<void>(); |
|
|
|
this.componentRef.instance.destroy$ = destroy$; |
|
|
|
destroy$.subscribe(() => { |
|
|
|
this.componentRef?.destroy(); |
|
|
|
this.componentRef = null; |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
canCreateCustomError(status: ErrorScreenErrorCodes): boolean { |
|
|
|
return !!( |
|
|
|
this.httpErrorConfig?.errorScreen?.component && |
|
|
|
this.httpErrorConfig?.errorScreen?.forWhichErrors && |
|
|
|
this.httpErrorConfig?.errorScreen?.forWhichErrors.indexOf(status) > -1 |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
protected filterRestErrors = ({ status }: HttpErrorResponse): boolean => { |
|
|
|
if (typeof status !== 'number') return false; |
|
|
|
|
|
|
|
@ -353,12 +95,4 @@ export class ErrorHandler { |
|
|
|
this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === status) < 0 |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
protected filterRouteErrors = (navigationError: NavigationError): boolean => { |
|
|
|
return ( |
|
|
|
navigationError.error?.message?.indexOf('Cannot match') > -1 && |
|
|
|
!!this.httpErrorConfig.skipHandledErrorCodes && |
|
|
|
this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === 404) < 0 |
|
|
|
); |
|
|
|
}; |
|
|
|
} |
|
|
|
|