Browse Source

Merge pull request #16853 from abpframework/refactor-handle-error-handler

Refactor handle error handler
pull/17341/head
Masum ULU 3 years ago
committed by GitHub
parent
commit
56d5c0e7ef
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 53
      npm/ng-packs/packages/theme-shared/src/lib/constants/default-errors.ts
  2. 356
      npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts
  3. 13
      npm/ng-packs/packages/theme-shared/src/lib/models/common.ts
  4. 29
      npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts
  5. 1
      npm/ng-packs/packages/theme-shared/src/lib/providers/index.ts
  6. 41
      npm/ng-packs/packages/theme-shared/src/lib/services/abp-format-error-handler.service.ts
  7. 95
      npm/ng-packs/packages/theme-shared/src/lib/services/create-error-component.service.ts
  8. 6
      npm/ng-packs/packages/theme-shared/src/lib/services/index.ts
  9. 41
      npm/ng-packs/packages/theme-shared/src/lib/services/router-error-handler.service.ts
  10. 94
      npm/ng-packs/packages/theme-shared/src/lib/services/status-code-error-handler.service.ts
  11. 24
      npm/ng-packs/packages/theme-shared/src/lib/services/tenant-resolve-error-handler.service.ts
  12. 35
      npm/ng-packs/packages/theme-shared/src/lib/services/unknown-status-code-error-handler.service.ts
  13. 5
      npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts
  14. 10
      npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts
  15. 6
      npm/ng-packs/packages/theme-shared/src/lib/tokens/http-error.token.ts
  16. 26
      npm/ng-packs/packages/theme-shared/src/lib/utils/error.utils.ts
  17. 1
      npm/ng-packs/packages/theme-shared/src/lib/utils/index.ts
  18. 1
      npm/ng-packs/packages/theme-shared/src/public-api.ts

53
npm/ng-packs/packages/theme-shared/src/lib/constants/default-errors.ts

@ -0,0 +1,53 @@
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',
},
};
export const CUSTOM_HTTP_ERROR_HANDLER_PRIORITY = Object.freeze({
veryLow: -99,
low: -9,
normal: 0,
high: 9,
veryHigh: 99,
});

356
npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts

@ -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
);
};
}

13
npm/ng-packs/packages/theme-shared/src/lib/models/common.ts

@ -20,10 +20,11 @@ export interface HttpErrorConfig {
hideCloseIcon?: boolean;
};
}
export type HttpErrorHandler = (
injector: Injector,
httpError: HttpErrorResponse,
) => Observable<any>;
export type HttpErrorHandler<T = any> = (httpError: HttpErrorResponse) => Observable<T>;
export type LocaleDirection = 'ltr' | 'rtl';
export interface CustomHttpErrorHandlerService {
readonly priority: number;
canHandle(error: unknown): boolean;
execute();
}

29
npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts

@ -0,0 +1,29 @@
import { Provider } from '@angular/core';
import { CUSTOM_ERROR_HANDLERS } from '../tokens';
import { TenantResolveErrorHandlerService } from '../services/tenant-resolve-error-handler.service';
import { AbpFormatErrorHandlerService } from '../services/abp-format-error-handler.service';
import { StatusCodeErrorHandlerService } from '../services/status-code-error-handler.service';
import { UnknownStatusCodeErrorHandlerService } from '../services/unknown-status-code-error-handler.service';
export const ERROR_HANDLERS_PROVIDERS: Provider[] = [
{
provide: CUSTOM_ERROR_HANDLERS,
multi: true,
useClass: TenantResolveErrorHandlerService,
},
{
provide: CUSTOM_ERROR_HANDLERS,
multi: true,
useClass: AbpFormatErrorHandlerService,
},
{
provide: CUSTOM_ERROR_HANDLERS,
multi: true,
useClass: StatusCodeErrorHandlerService,
},
{
provide: CUSTOM_ERROR_HANDLERS,
multi: true,
useClass: UnknownStatusCodeErrorHandlerService,
},
];

1
npm/ng-packs/packages/theme-shared/src/lib/providers/index.ts

@ -1,2 +1,3 @@
export * from './ng-bootstrap-config.provider';
export * from './route.provider';
export * from './error-handlers.provider';

41
npm/ng-packs/packages/theme-shared/src/lib/services/abp-format-error-handler.service.ts

@ -0,0 +1,41 @@
import { inject, Injectable } from '@angular/core';
import { AuthService } from '@abp/ng.core';
import { HttpErrorResponse } from '@angular/common/http';
import { getErrorFromRequestBody } from '../utils/error.utils';
import { CustomHttpErrorHandlerService } from '../models/common';
import { ConfirmationService } from '../services/confirmation.service';
import { CUSTOM_HTTP_ERROR_HANDLER_PRIORITY } from '../constants/default-errors';
@Injectable({ providedIn: 'root' })
export class AbpFormatErrorHandlerService implements CustomHttpErrorHandlerService {
readonly priority = CUSTOM_HTTP_ERROR_HANDLER_PRIORITY.high;
private confirmationService = inject(ConfirmationService);
private authService = inject(AuthService);
private error: HttpErrorResponse | undefined = undefined;
private navigateToLogin() {
return this.authService.navigateToLogin();
}
canHandle(error: unknown): boolean {
if (error instanceof HttpErrorResponse && error.headers.get('_AbpErrorFormat')) {
this.error = error;
return true;
}
return false;
}
execute() {
const { message, title } = getErrorFromRequestBody(this.error?.error?.error);
this.confirmationService
.error(message, title, {
hideCancelBtn: true,
yesText: 'AbpAccount::Close',
})
.subscribe(() => {
if (this.error?.status === 401) {
this.navigateToLogin();
}
});
}
}

95
npm/ng-packs/packages/theme-shared/src/lib/services/create-error-component.service.ts

@ -0,0 +1,95 @@
import {
ApplicationRef,
ComponentFactoryResolver,
ComponentRef,
EmbeddedViewRef,
inject,
Injectable,
Injector,
RendererFactory2,
} from '@angular/core';
import { Subject } from 'rxjs';
import { ResolveEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
import { RouterEvents } from '@abp/ng.core';
import { HTTP_ERROR_CONFIG } from '../tokens/http-error.token';
import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component';
import { ErrorScreenErrorCodes } from '../models/common';
@Injectable({ providedIn: 'root' })
export class CreateErrorComponentService {
protected rendererFactory = inject(RendererFactory2);
protected cfRes = inject(ComponentFactoryResolver);
private routerEvents = inject(RouterEvents);
private injector = inject(Injector);
private httpErrorConfig = inject(HTTP_ERROR_CONFIG);
componentRef: ComponentRef<HttpErrorWrapperComponent> | null = null;
private getErrorHostElement() {
return document.body;
}
public canCreateCustomError(status: ErrorScreenErrorCodes) {
return !!(
this.httpErrorConfig?.errorScreen?.component &&
this.httpErrorConfig?.errorScreen?.forWhichErrors &&
this.httpErrorConfig?.errorScreen?.forWhichErrors.indexOf(status) > -1
);
}
constructor() {
this.listenToRouterDataResolved();
}
protected listenToRouterDataResolved() {
this.routerEvents
.getEvents(ResolveEnd)
.pipe(filter(() => !!this.componentRef))
.subscribe(() => {
this.componentRef?.destroy();
this.componentRef = null;
});
}
private isCloseIconHidden() {
return !!this.httpErrorConfig.errorScreen?.hideCloseIcon;
}
execute(instance: Partial<HttpErrorWrapperComponent>) {
const renderer = this.rendererFactory.createRenderer(null, null);
const hostElement = this.getErrorHostElement();
const host = renderer.selectRootElement(hostElement, 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.isCloseIconHidden();
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;
});
}
}

6
npm/ng-packs/packages/theme-shared/src/lib/services/index.ts

@ -3,3 +3,9 @@ export * from './nav-items.service';
export * from './page-alert.service';
export * from './toaster.service';
export * from './user-menu.service';
export * from './create-error-component.service';
export * from './abp-format-error-handler.service';
export * from './tenant-resolve-error-handler.service';
export * from './status-code-error-handler.service';
export * from './unknown-status-code-error-handler.service';
export * from './router-error-handler.service';

41
npm/ng-packs/packages/theme-shared/src/lib/services/router-error-handler.service.ts

@ -0,0 +1,41 @@
import { inject, Injectable } from '@angular/core';
import { filter } from 'rxjs/operators';
import { RouterEvents } from '@abp/ng.core';
import { NavigationError } from '@angular/router';
import { HTTP_ERROR_CONFIG } from '../tokens/';
import { CreateErrorComponentService } from '../services';
import { DEFAULT_ERROR_LOCALIZATIONS, DEFAULT_ERROR_MESSAGES } from '../constants/default-errors';
@Injectable({ providedIn: 'root' })
export class RouterErrorHandlerService {
private readonly routerEvents = inject(RouterEvents);
private httpErrorConfig = inject(HTTP_ERROR_CONFIG);
private createErrorComponentService = inject(CreateErrorComponentService);
listen() {
this.routerEvents
.getNavigationEvents('Error')
.pipe(filter(this.filterRouteErrors))
.subscribe(() => this.show404Page());
}
protected filterRouteErrors = (navigationError: NavigationError): boolean => {
return (
navigationError.error?.message?.indexOf('Cannot match') > -1 &&
!!this.httpErrorConfig.skipHandledErrorCodes &&
this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === 404) < 0
);
};
show404Page() {
const instance = {
title: {
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.title,
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title,
},
status: 404,
};
this.createErrorComponentService.execute(instance);
}
}

94
npm/ng-packs/packages/theme-shared/src/lib/services/status-code-error-handler.service.ts

@ -0,0 +1,94 @@
import { Confirmation, CustomHttpErrorHandlerService } from '../models';
import {
CUSTOM_HTTP_ERROR_HANDLER_PRIORITY,
DEFAULT_ERROR_LOCALIZATIONS,
DEFAULT_ERROR_MESSAGES,
} from '../constants/default-errors';
import { AuthService, LocalizationParam } from '@abp/ng.core';
import { Observable } from 'rxjs';
import { inject, Injectable } from '@angular/core';
import { ConfirmationService } from './confirmation.service';
import { CreateErrorComponentService } from './create-error-component.service';
@Injectable({ providedIn: 'root' })
export class StatusCodeErrorHandlerService implements CustomHttpErrorHandlerService {
private readonly confirmationService = inject(ConfirmationService);
private readonly createErrorComponentService = inject(CreateErrorComponentService);
private readonly authService = inject(AuthService);
readonly priority = CUSTOM_HTTP_ERROR_HANDLER_PRIORITY.normal;
private status: typeof this.handledStatusCodes[number];
private readonly handledStatusCodes = [401, 403, 404, 500] as const;
canHandle({ status }): boolean {
this.status = status;
return this.handledStatusCodes.indexOf(status) > -1;
}
execute() {
const key = `defaultError${this.status}`;
const title = {
key: DEFAULT_ERROR_LOCALIZATIONS[key]?.title,
defaultValue: DEFAULT_ERROR_MESSAGES[key]?.title,
};
const message = {
key: DEFAULT_ERROR_LOCALIZATIONS[key]?.details,
defaultValue: DEFAULT_ERROR_MESSAGES[key]?.details,
};
const canCreateCustomError = this.createErrorComponentService.canCreateCustomError(this.status);
switch (this.status) {
case 401:
case 404:
if (canCreateCustomError) {
this.showPage();
break;
}
this.showConfirmation(title, message).subscribe(() => {
if (this.status === 401) {
this.navigateToLogin();
}
});
break;
case 403:
case 500:
this.showPage();
break;
}
}
private navigateToLogin() {
this.authService.navigateToLogin();
}
protected showConfirmation(
message: LocalizationParam,
title: LocalizationParam,
): Observable<Confirmation.Status> {
return this.confirmationService.error(message, title, {
hideCancelBtn: true,
yesText: 'AbpAccount::Close',
});
}
protected showPage() {
const key = `defaultError${this.status}`;
const instance = {
title: {
key: DEFAULT_ERROR_LOCALIZATIONS[key]?.title,
defaultValue: DEFAULT_ERROR_MESSAGES[key]?.title,
},
details: {
key: DEFAULT_ERROR_LOCALIZATIONS[key]?.details,
defaultValue: DEFAULT_ERROR_MESSAGES[key]?.details,
},
status: this.status,
};
const shouldRemoveDetail = [401, 404].indexOf(this.status) > -1;
if (shouldRemoveDetail) {
delete instance.details;
}
this.createErrorComponentService.execute(instance);
}
}

24
npm/ng-packs/packages/theme-shared/src/lib/services/tenant-resolve-error-handler.service.ts

@ -0,0 +1,24 @@
import { CustomHttpErrorHandlerService } from '../models/common';
import { inject, Injectable } from '@angular/core';
import { AuthService, SessionStateService } from '@abp/ng.core';
import { HttpErrorResponse } from '@angular/common/http';
import { CUSTOM_HTTP_ERROR_HANDLER_PRIORITY } from '../constants/default-errors';
@Injectable({ providedIn: 'root' })
export class TenantResolveErrorHandlerService implements CustomHttpErrorHandlerService {
protected readonly sessionService = inject(SessionStateService);
readonly priority = CUSTOM_HTTP_ERROR_HANDLER_PRIORITY.high;
private authService = inject(AuthService);
private isTenantResolveError(error: unknown) {
return error instanceof HttpErrorResponse && !!error.headers.get('Abp-Tenant-Resolve-Error');
}
canHandle(error: unknown): boolean {
return this.isTenantResolveError(error);
}
execute() {
this.sessionService.setTenant(null);
this.authService.logout().subscribe();
}
}

35
npm/ng-packs/packages/theme-shared/src/lib/services/unknown-status-code-error-handler.service.ts

@ -0,0 +1,35 @@
import { CustomHttpErrorHandlerService } from '../models';
import {
CUSTOM_HTTP_ERROR_HANDLER_PRIORITY,
DEFAULT_ERROR_LOCALIZATIONS,
DEFAULT_ERROR_MESSAGES,
} from '../constants/default-errors';
import { inject, Injectable } from '@angular/core';
import { CreateErrorComponentService } from './create-error-component.service';
@Injectable({ providedIn: 'root' })
export class UnknownStatusCodeErrorHandlerService implements CustomHttpErrorHandlerService {
readonly priority = CUSTOM_HTTP_ERROR_HANDLER_PRIORITY.normal;
private statusText = 'Unknown Error';
private message = '';
private createErrorComponentService = inject(CreateErrorComponentService);
canHandle(error: { status: number; statusText: string; message: string } | undefined): boolean {
if (error && error.status === 0 && error.statusText === this.statusText) {
this.message = error.message;
return true;
}
return false;
}
execute() {
this.createErrorComponentService.execute({
title: {
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title,
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title,
},
details: this.message,
isHomeShow: false,
});
}
}

5
npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts

@ -7,7 +7,8 @@ import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { OAuthService } from 'angular-oauth2-oidc';
import { of, Subject } from 'rxjs';
import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component';
import { DEFAULT_ERROR_LOCALIZATIONS, DEFAULT_ERROR_MESSAGES, ErrorHandler } from '../handlers';
import { ErrorHandler } from '../handlers';
import { DEFAULT_ERROR_LOCALIZATIONS, DEFAULT_ERROR_MESSAGES } from '../constants/default-errors';
import { ConfirmationService } from '../services';
import { httpErrorConfigFactory } from '../tokens/http-error.token';
@ -16,7 +17,7 @@ const reporter$ = new Subject();
@NgModule({
exports: [HttpErrorWrapperComponent],
declarations: [HttpErrorWrapperComponent],
entryComponents: [HttpErrorWrapperComponent],
//entryComponents: [HttpErrorWrapperComponent],
imports: [CoreTestingModule],
})
class MockModule {}

10
npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts

@ -29,7 +29,7 @@ import { NgxDatatableListDirective } from './directives/ngx-datatable-list.direc
import { DocumentDirHandlerService } from './handlers/document-dir.handler';
import { ErrorHandler } from './handlers/error.handler';
import { RootParams } from './models/common';
import { NG_BOOTSTRAP_CONFIG_PROVIDERS } from './providers';
import { ERROR_HANDLERS_PROVIDERS, NG_BOOTSTRAP_CONFIG_PROVIDERS } from './providers';
import { THEME_SHARED_ROUTE_PROVIDERS } from './providers/route.provider';
import { THEME_SHARED_APPEND_CONTENT } from './tokens/append-content.token';
import { HTTP_ERROR_CONFIG, httpErrorConfigFactory } from './tokens/http-error.token';
@ -58,7 +58,7 @@ const declarationsWithExports = [
ModalCloseDirective,
AbpVisibleDirective,
FormInputComponent,
FormCheckboxComponent
FormCheckboxComponent,
];
@NgModule({
@ -69,7 +69,6 @@ const declarationsWithExports = [
NgbPaginationModule,
EllipsisModule,
CardModule,
],
declarations: [...declarationsWithExports, HttpErrorWrapperComponent],
exports: [
@ -77,11 +76,11 @@ const declarationsWithExports = [
EllipsisModule,
NgxValidateCoreModule,
CardModule,
...declarationsWithExports
...declarationsWithExports,
],
providers: [DatePipe],
})
export class BaseThemeSharedModule { }
export class BaseThemeSharedModule {}
@NgModule({
imports: [BaseThemeSharedModule],
@ -144,6 +143,7 @@ export class ThemeSharedModule {
...(confirmationIcons || {}),
},
},
ERROR_HANDLERS_PROVIDERS,
],
};
}

6
npm/ng-packs/packages/theme-shared/src/lib/tokens/http-error.token.ts

@ -1,5 +1,5 @@
import { InjectionToken } from '@angular/core';
import { HttpErrorConfig, HttpErrorHandler } from '../models/common';
import { CustomHttpErrorHandlerService, HttpErrorConfig, HttpErrorHandler } from '../models/common';
export function httpErrorConfigFactory(config = {} as HttpErrorConfig) {
if (config.errorScreen && config.errorScreen.component && !config.errorScreen.forWhichErrors) {
@ -16,3 +16,7 @@ export function httpErrorConfigFactory(config = {} as HttpErrorConfig) {
export const HTTP_ERROR_CONFIG = new InjectionToken<HttpErrorConfig>('HTTP_ERROR_CONFIG');
export const HTTP_ERROR_HANDLER = new InjectionToken<HttpErrorHandler>('HTTP_ERROR_HANDLER');
export const CUSTOM_ERROR_HANDLERS = new InjectionToken<CustomHttpErrorHandlerService[]>(
'CUSTOM_ERROR_HANDLERS',
);

26
npm/ng-packs/packages/theme-shared/src/lib/utils/error.utils.ts

@ -0,0 +1,26 @@
import { LocalizationParam } from '@abp/ng.core';
import { DEFAULT_ERROR_LOCALIZATIONS, DEFAULT_ERROR_MESSAGES } from '../constants/default-errors';
export function getErrorFromRequestBody(body: { details?: string; message?: string } | undefined) {
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 = {
key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title,
defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title,
};
title = '';
}
return { message, title };
}

1
npm/ng-packs/packages/theme-shared/src/lib/utils/index.ts

@ -1,2 +1,3 @@
export * from './date-parser-formatter';
export * from './validation-utils';
export * from './error.utils';

1
npm/ng-packs/packages/theme-shared/src/public-api.ts

@ -5,6 +5,7 @@
export * from './lib/animations';
export * from './lib/components';
export * from './lib/constants/validation';
export * from './lib/constants/default-errors';
export * from './lib/directives';
export * from './lib/enums';
export * from './lib/handlers';

Loading…
Cancel
Save