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:
+
+ 
+
+ - 404 not found errors is handled. When a 404 error occurs, `Error404Component` will be appended to the `
` as shown below:
+
+
+
+ - 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');