diff --git a/docs/en/UI/Angular/HTTP-Requests.md b/docs/en/UI/Angular/HTTP-Requests.md index ab1ac04097..aeb55578b8 100644 --- a/docs/en/UI/Angular/HTTP-Requests.md +++ b/docs/en/UI/Angular/HTTP-Requests.md @@ -27,13 +27,13 @@ An `HttpInterceptor` is able to catch `HttpErrorResponse`  and can be used for ## RestService -ABP core module has a utility service for HTTP requests: `RestService`. Unless explicitly configured otherwise, it catches HTTP errors and dispatches a `RestOccurError` action. This action is then captured by the `ErrorHandler` introduced by the `ThemeSharedModule`. Since you should already import this module in your app, when the `RestService` is used, all HTTP errors get automatically handled by deafult. +ABP core module has a utility service for HTTP requests: `RestService`. Unless explicitly configured otherwise, it catches HTTP errors and dispatches a `RestOccurError` action. This action is then captured by the `ErrorHandler` introduced by the `ThemeSharedModule`. Since you should already import this module in your app, when the `RestService` is used, all HTTP errors get automatically handled by default. ### Getting Started with RestService -In order to use the `RestService`, you must inject it in your class as a dependency. +In order to use the `RestService`, you must inject it in your class as a dependency. ```js import { RestService } from '@abp/ng.core'; @@ -200,6 +200,101 @@ getSomeCustomHeaderValue() { } ``` +You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10). -You may find `Rest.Observe` enum [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L10). +## HTTP Error Handling + +When the `RestService` is used, all HTTP errors are automatically handled by `ErrorHandler` which is a service that exposed by the `@abp/ng.theme.shared` package. + +### Custom HTTP Error Handler + +A custom HTTP error handler can be registered to an injection token named `HTTP_ERROR_HANDLER`. If a custom handler function is registered, the `ErrorHandler` executes that function. + +See an example: + +```js +// http-error-handler.ts +import { ContentProjectionService, PROJECTION_STRATEGY } from '@abp/ng.core'; +import { ToasterService } from '@abp/ng.theme.shared'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Injector } from '@angular/core'; +import { throwError } from 'rxjs'; +import { Error404Component } from './error404/error404.component'; + +export function handleHttpErrors(injector: Injector, httpError: HttpErrorResponse) { + if (httpError.status === 400) { + const toaster = injector.get(ToasterService); + toaster.error(httpError.error?.error?.message || 'Bad request!', '400'); + return; + } + + if (httpError.status === 404) { + const contentProjection = injector.get(ContentProjectionService); + contentProjection.projectContent(PROJECTION_STRATEGY.AppendComponentToBody(Error404Component)); + return; + } + + return throwError(httpError); +} + +// app.module.ts +import { Error404Component } from './error404/error404.component'; +import { handleHttpErrors } from './http-error-handling'; +import { HTTP_ERROR_HANDLER, ... } from '@abp/ng.theme.shared'; + +@NgModule({ + // ... + providers: [ + // ... + { provide: HTTP_ERROR_HANDLER, useValue: handleHttpErrors } + ], + declarations: [ + //... + Error404Component], +}) +export class AppModule {} +``` + +In the example above: + + - Created a function named `handleHttpErrors` and defined as value of the `HTTP_ERROR_HANDLER` provider in app.module. After this, the function executes when an HTTP error occurs. + - 400 bad request errors is handled. When a 400 error occurs, backend error message will be displayed as shown below: + + ![custom-error-handler-toaster-message](images/custom-error-handler-toaster-message.jpg) + + - 404 not found errors is handled. When a 404 error occurs, `Error404Component` will be appended to the `` as shown below: + +![custom-error-handler-404-component](images/custom-error-handler-404-component.jpg) + + - Since `throwError(httpError)` is returned at bottom of the `handleHttpErrors`, the `ErrorHandler` will handle the HTTP errors except 400 and 404 errors. + + +**Note 1:** If you put `return` to next line of handling an error, default error handling will not work for that error. + +```js +export function handleHttpErrors(injector: Injector, httpError: HttpErrorResponse) { + if (httpError.status === 403) { + // handle 403 errors here + return; // put return to skip default error handling + } +} +``` + +**Note 2:** If you put `return throwError(httpError)`, default error handling will work. + - `throwError` is a function. It can be imported from `rxjs`. + - `httpError` is the second parameter of the error handler function which is registered to the `HTTP_ERROR_HANDLER` provider. Type of the `httpError` is `HttpErrorResponse`. + +```js +import { throwError } from 'rxjs'; + +export function handleHttpErrors(injector: Injector, httpError: HttpErrorResponse) { + if (httpError.status === 500) { + // handle 500 errors here + return; + } + + // you can return the throwError(httpError) at bottom of the function to run the default handler of ABP for HTTP errors that you didn't handle above. + return throwError(httpError) +} +``` diff --git a/docs/en/UI/Angular/images/custom-error-handler-404-component.jpg b/docs/en/UI/Angular/images/custom-error-handler-404-component.jpg new file mode 100644 index 0000000000..e4b2f15c50 Binary files /dev/null and b/docs/en/UI/Angular/images/custom-error-handler-404-component.jpg differ diff --git a/docs/en/UI/Angular/images/custom-error-handler-toaster-message.jpg b/docs/en/UI/Angular/images/custom-error-handler-toaster-message.jpg new file mode 100644 index 0000000000..c1cced8237 Binary files /dev/null and b/docs/en/UI/Angular/images/custom-error-handler-toaster-message.jpg differ diff --git a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts index 1835b80eef..1001c98085 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts @@ -12,13 +12,14 @@ import { } from '@angular/core'; import { NavigationError, ResolveEnd } from '@angular/router'; import { Actions, ofActionSuccessful } from '@ngxs/store'; -import { Observable, Subject } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { Observable, of, Subject, throwError } from 'rxjs'; +import { catchError, filter, map, switchMap } from 'rxjs/operators'; import snq from 'snq'; import { HttpErrorWrapperComponent } from '../components/http-error-wrapper/http-error-wrapper.component'; import { ErrorScreenErrorCodes, HttpErrorConfig } 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: { @@ -70,28 +71,32 @@ export const DEFAULT_ERROR_LOCALIZATIONS = { export class ErrorHandler { componentRef: ComponentRef; + protected httpErrorHandler = this.injector.get(HTTP_ERROR_HANDLER, (_, err: HttpErrorResponse) => + throwError(err), + ); + constructor( - private actions: Actions, - private routerEvents: RouterEvents, - private confirmationService: ConfirmationService, - private cfRes: ComponentFactoryResolver, - private rendererFactory: RendererFactory2, - private injector: Injector, - @Inject('HTTP_ERROR_CONFIG') private httpErrorConfig: HttpErrorConfig, + protected actions: Actions, + protected routerEvents: RouterEvents, + protected confirmationService: ConfirmationService, + protected cfRes: ComponentFactoryResolver, + protected rendererFactory: RendererFactory2, + protected injector: Injector, + @Inject('HTTP_ERROR_CONFIG') protected httpErrorConfig: HttpErrorConfig, ) { this.listenToRestError(); this.listenToRouterError(); this.listenToRouterDataResolved(); } - private listenToRouterError() { + protected listenToRouterError() { this.routerEvents .getNavigationEvents('Error') .pipe(filter(this.filterRouteErrors)) .subscribe(() => this.show404Page()); } - private listenToRouterDataResolved() { + protected listenToRouterDataResolved() { this.routerEvents .getEvents(ResolveEnd) .pipe(filter(() => !!this.componentRef)) @@ -101,113 +106,127 @@ export class ErrorHandler { }); } - private listenToRestError() { + protected listenToRestError() { this.actions .pipe( ofActionSuccessful(RestOccurError), map(action => action.payload), filter(this.filterRestErrors), + switchMap(this.executeErrorHandler), ) - .subscribe(err => { - const body = snq(() => err.error.error, { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, - }); + .subscribe(); + } - if (err instanceof HttpErrorResponse && err.headers.get('_AbpErrorFormat')) { - const confirmation$ = this.showError(null, null, body); + private executeErrorHandler = error => { + const returnValue = this.httpErrorHandler(this.injector, error); - 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, + return (returnValue instanceof Observable ? returnValue : of(null)).pipe( + catchError(err => { + this.handleError(err); + return of(null); + }), + ); + }; + + private handleError(err: any) { + const body = snq(() => err.error.error, { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + }); + + if (err instanceof HttpErrorResponse && err.headers.get('_AbpErrorFormat')) { + const confirmation$ = this.showError(null, null, body); + + 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, }, - details: { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError500.details, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError500.details, + { + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError401.details, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError401.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( + ).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.defaultError.details, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.details, + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.details, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.details, }, { - key: DEFAULT_ERROR_LOCALIZATIONS.defaultError.title, - defaultValue: DEFAULT_ERROR_MESSAGES.defaultError.title, + key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.title, + defaultValue: DEFAULT_ERROR_MESSAGES.defaultError404.title, }, ); - break; + 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 show401Page() { + protected show401Page() { this.createErrorComponent({ title: { key: DEFAULT_ERROR_LOCALIZATIONS.defaultError401.title, @@ -217,7 +236,7 @@ export class ErrorHandler { }); } - private show404Page() { + protected show404Page() { this.createErrorComponent({ title: { key: DEFAULT_ERROR_LOCALIZATIONS.defaultError404.title, @@ -227,7 +246,7 @@ export class ErrorHandler { }); } - private showError( + protected showError( message?: LocalizationParam, title?: LocalizationParam, body?: any, @@ -256,7 +275,7 @@ export class ErrorHandler { }); } - private navigateToLogin() { + protected navigateToLogin() { this.injector.get(AuthService).initLogin(); } @@ -304,13 +323,13 @@ export class ErrorHandler { ); } - private filterRestErrors = ({ status }: HttpErrorResponse): boolean => { + protected filterRestErrors = ({ status }: HttpErrorResponse): boolean => { if (typeof status !== 'number') return false; return this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === status) < 0; }; - private filterRouteErrors = (navigationError: NavigationError): boolean => { + protected filterRouteErrors = (navigationError: NavigationError): boolean => { return ( snq(() => navigationError.error.message.indexOf('Cannot match') > -1) && this.httpErrorConfig.skipHandledErrorCodes.findIndex(code => code === 404) < 0 diff --git a/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts b/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts index 64047e0db7..0e8dd9e592 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/models/common.ts @@ -1,5 +1,7 @@ -import { Type } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { Injector, Type } from '@angular/core'; import { Validation } from '@ngx-validate/core'; +import { Observable } from 'rxjs'; export interface RootParams { httpErrorConfig: HttpErrorConfig; @@ -17,4 +19,9 @@ export interface HttpErrorConfig { }; } +export type HttpErrorHandler = ( + injector: Injector, + httpError: HttpErrorResponse, +) => Observable; + export type LocaleDirection = 'ltr' | 'rtl'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tokens/http-error.token.ts b/npm/ng-packs/packages/theme-shared/src/lib/tokens/http-error.token.ts index 62f2d2d593..39cb43a350 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tokens/http-error.token.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tokens/http-error.token.ts @@ -1,5 +1,5 @@ import { InjectionToken } from '@angular/core'; -import { HttpErrorConfig } from '../models/common'; +import { HttpErrorConfig, HttpErrorHandler } from '../models/common'; export function httpErrorConfigFactory(config = {} as HttpErrorConfig) { if (config.errorScreen && config.errorScreen.component && !config.errorScreen.forWhichErrors) { @@ -13,4 +13,6 @@ export function httpErrorConfigFactory(config = {} as HttpErrorConfig) { } as HttpErrorConfig; } -export const HTTP_ERROR_CONFIG = new InjectionToken('HTTP_ERROR_CONFIG'); +export const HTTP_ERROR_CONFIG = new InjectionToken('HTTP_ERROR_CONFIG'); + +export const HTTP_ERROR_HANDLER = new InjectionToken('HTTP_ERROR_HANDLER');