Browse Source

Merge pull request #18787 from abpframework/issue-18774

Fix / Enhancement For Error Handler, Update Documentation
pull/18861/head
Masum ULU 2 years ago
committed by GitHub
parent
commit
b16a6bbf6d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 197
      docs/en/UI/Angular/HTTP-Error-Handling.md
  2. 144
      docs/en/UI/Angular/HTTP-Requests.md
  3. 15
      docs/en/docs-nav.json
  4. 6
      npm/ng-packs/packages/core/src/lib/services/rest.service.ts
  5. 21
      npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts
  6. 4
      npm/ng-packs/packages/theme-shared/src/lib/models/common.ts
  7. 10
      npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts
  8. 4
      npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts
  9. 3
      npm/ng-packs/packages/theme-shared/src/lib/tokens/http-error.token.ts

197
docs/en/UI/Angular/HTTP-Error-Handling.md

@ -0,0 +1,197 @@
# HTTP Error Handling
When the `RestService` is used, all HTTP errors are reported to the [`HttpErrorReporterService`](./HTTP-Error-Reporter-Service), and then `ErrorHandler`, a service exposed by the `@abp/ng.theme.shared` package automatically handles the errors.
## Custom HTTP Error Handler
### Function Method `Deprecated`
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:
```ts
// 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 { of, EMPTY } 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 EMPTY;
}
if (httpError.status === 404) {
const contentProjection = injector.get(ContentProjectionService);
contentProjection.projectContent(PROJECTION_STRATEGY.AppendComponentToBody(Error404Component));
return EMPTY;
}
return of(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.
- Since `of(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 EMPTY` to next line of handling an error, default error handling will not work for that error. [EMPTY](https://rxjs.dev/api/index/const/EMPTY) can be imported from `rxjs`.
```ts
export function handleHttpErrors(
injector: Injector,
httpError: HttpErrorResponse
) {
if (httpError.status === 403) {
// handle 403 errors here
return EMPTY; // put return EMPTY to skip default error handling
}
}
```
**Note 2:** If you put `return of(httpError)`, default error handling will work.
- `of` 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`.
```ts
import { of } from "rxjs";
export function handleHttpErrors(
injector: Injector,
httpError: HttpErrorResponse
) {
if (httpError.status === 500) {
// handle 500 errors here
}
// you can return the of(httpError) at bottom of the function to run the default handler of ABP for HTTP errors that you didn't handle above.
return of(httpError);
}
```
### Service Method
You can provide **more than one handler** with services, a custom HTTP error handler service can be registered with injection token named **`CUSTOM_ERROR_HANDLERS`**. ABP has some default [error handlers](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts).
### How To Add New Handler Service
ABP error handler services are implements the interface of **CustomHttpErrorHandlerService**.
**Interface of `CUSTOM_ERROR_HANDLERS`**
```ts
interface CustomHttpErrorHandlerService {
readonly priority: number;
canHandle(error: unknown): boolean;
execute(): void;
}
```
- **`priority`** ABP sorts the services according to the number of the priority variable. Higher priority will be checked first.
- **`canHandle`** Check if the service can handle the error. Returns boolean.
- **`execute`** If the service can handle the error, then run the execute method.
**In Summary**
- Services are sorted by their priority number.
- Start from highest priority service and run canHandle() method. Pick the service if can handle the error, if not check next service.
- If the service found, run the execute method of a service. Done.
See an example:
```ts
// custom-error-handler.service.ts
import { inject, Injectable } from "@angular/core";
import { HttpErrorResponse } from "@angular/common/http";
import { CustomHttpErrorHandlerService } from "@abp/ng.theme.shared";
import { CUSTOM_HTTP_ERROR_HANDLER_PRIORITY } from "@abp/ng.theme.shared";
import { ToasterService } from "@abp/ng.theme.shared";
@Injectable({ providedIn: "root" })
export class MyCustomErrorHandlerService
implements CustomHttpErrorHandlerService
{
// You can write any number here, ex: 9999
readonly priority = CUSTOM_HTTP_ERROR_HANDLER_PRIORITY.veryHigh;
protected readonly toaster = inject(ToasterService);
private error: HttpErrorResponse | undefined = undefined;
// What kind of error should be handled by this service? You can decide it in this method. If error is suitable to your case then return true; otherwise return false.
canHandle(error: unknown): boolean {
if (error instanceof HttpErrorResponse && error.status === 400) {
this.error = error;
return true;
}
return false;
}
// If this service is picked from ErrorHandler, this execute method will be called.
execute() {
this.toaster.error(
this.error.error?.error?.message || "Bad request!",
"400"
);
}
}
```
```ts
// app.module.ts
import { CUSTOM_ERROR_HANDLERS, ... } from '@abp/ng.theme.shared';
import { MyCustomErrorHandlerService } from './custom-error-handler.service';
@NgModule({
// ...
providers: [
// ...
{
provide: CUSTOM_ERROR_HANDLERS,
useExisting: MyCustomErrorHandlerService,
multi: true,
}
]
})
export class AppModule {}
```
In the example above:
- Created a service named `MyCustomErrorHandlerService`, and provided via `useExisting` key because we dont want another instance of it. And set `multi` key to true because ABP default error handlers are also provided with **CUSTOM_ERROR_HANDLERS** injection token.
- 400 errors are handled from custom `MyCustomErrorHandlerService`. 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)
### Notes
- If your service cannot handle the error. Then ABP will check the next Error Service.
- If none of the service handle the error. Then basic confirmation message about the error will be shown to the user.
- You can provide more than one service, with CUSTOM_ERROR_HANDLER injection token.
- If you want your custom service to be evaluated (checked) earlier, set the priority variable high.

144
docs/en/UI/Angular/HTTP-Requests.md

@ -1,7 +1,5 @@
# How to Make HTTP Requests
## About HttpClient
Angular has the amazing [HttpClient](https://angular.io/guide/http) for communication with backend services. It is a layer on top and a simplified representation of [XMLHttpRequest Web API](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest). It also is the recommended agent by Angular for any HTTP request. There is nothing wrong with using the `HttpClient` in your ABP project.
@ -23,14 +21,10 @@ Although clear and flexible, handling errors this way is repetitive work, even w
An `HttpInterceptor` is able to catch `HttpErrorResponse` and can be used for a centralized error handling. Nevertheless, cases where default error handler, therefore the interceptor, must be disabled require additional work and comprehension of Angular internals. Check [this issue](https://github.com/angular/angular/issues/20203) for details.
## 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 default.
### Getting Started with RestService
In order to use the `RestService`, you must inject it in your class as a dependency.
@ -48,11 +42,9 @@ class DemoService {
You do not have to provide the `RestService` at module or component/directive level, because it is already **provided in root**.
### How to Make a Request with RestService
You can use the `request` method of the `RestService` is for HTTP requests. Here is an example:
You can use the `request` method of the `RestService` is for HTTP requests. Here is an example:
```js
getFoo(id: number) {
@ -65,8 +57,6 @@ getFoo(id: number) {
}
```
The `request` method always returns an `Observable<T>`. Therefore you can do the following wherever you use `getFoo` method:
```js
@ -79,12 +69,8 @@ doSomethingWithFoo(id: number) {
}
```
**You do not have to worry about unsubscription.** The `RestService` uses `HttpClient` behind the scenes, so every observable it returns is a finite observable, i.e. it closes subscriptions automatically upon success or error.
As you see, `request` method gets a request options object with `Rest.Request<T>` type. This generic type expects the interface of the request body. You may pass `null` when there is no body, like in a `GET` or a `DELETE` request. Here is an example where there is one:
```js
@ -99,11 +85,7 @@ postFoo(body: Foo) {
}
```
You may [check here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23) for complete `Rest.Request<T>` type, which has only a few changes compared to [HttpRequest](https://angular.io/api/common/http/HttpRequest) class in Angular.
You may [check here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/rest.ts#L23) for complete `Rest.Request<T>` type, which has only a few changes compared to [HttpRequest](https://angular.io/api/common/http/HttpRequest) class in Angular.
### How to Disable Default Error Handler of RestService
@ -120,8 +102,6 @@ deleteFoo(id: number) {
}
```
`skipHandleError` config option, when set to `true`, disables the error handler and the returned observable starts throwing an error that you can catch in your subscription.
```js
@ -137,14 +117,10 @@ removeFooFromList(id: number) {
}
```
### How to Get a Specific API Endpoint From Application Config
Another nice config option that `request` method receives is `apiName` (available as of v2.4), which can be used to get a specific module endpoint from application configuration.
```js
putFoo(body: Foo, id: string) {
const request: Rest.Request<Foo> = {
@ -157,8 +133,6 @@ putFoo(body: Foo, id: string) {
}
```
`putFoo` above will request `https://localhost:44305/api/some/path/to/foo/{id}` as long as the environment variables are as follows:
```js
@ -167,19 +141,17 @@ putFoo(body: Foo, id: string) {
export const environment = {
apis: {
default: {
url: 'https://localhost:44305',
url: "https://localhost:44305",
},
foo: {
url: 'https://localhost:44305/api/some/path/to/foo',
url: "https://localhost:44305/api/some/path/to/foo",
},
},
/* rest of the environment variables here */
}
};
```
### How to Observe Response Object or HTTP Events Instead of Body
`RestService` assumes you are generally interested in the body of a response and, by default, sets `observe` property as `'body'`. However, there may be times you are rather interested in something else, such as a custom proprietary header. For that, the `request` method receives `observe` property in its config object.
@ -202,104 +174,6 @@ 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).
## HTTP Error Handling
When the `RestService` is used, all HTTP errors are reported to the [`HttpErrorReporterService`](./HTTP-Error-Reporter-Service), and then `ErrorHandler`, a service exposed by the `@abp/ng.theme.shared` package automatically handles the errors.
### 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 `<body>` 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)
}
```
### How to Skip HTTP interceptors and ABP headers
The ABP Framework adds several HTTP headers to the HttpClient, such as the "Auth token" or "tenant Id".
@ -309,4 +183,8 @@ The ABP Http interceptors check the value of the `IS_EXTERNAL_REQUEST` token. If
The `ExternalHttpClient` extends from `HTTPClient` and sets the `IS_EXTERNAL_REQUEST` context token to true.
When you are using `ExternalHttpClient` as HttpClient in your components, it does not add ABP-specific headers.
Note: With `IS_EXTERNAL_REQUEST` or without it, ABP loading service works.
Note: With `IS_EXTERNAL_REQUEST` or without it, ABP loading service works.
## See Also
- [HTTP Error Handling / Customization](./HTTP-Error-Handling)

15
docs/en/docs-nav.json

@ -1053,7 +1053,16 @@
},
{
"text": "HTTP Requests",
"path": "UI/Angular/HTTP-Requests.md"
"items": [
{
"text": "How to Make HTTP Requests",
"path": "UI/Angular/HTTP-Requests.md"
},
{
"text": "HTTP Error Handling / Customization",
"path": "UI/Angular/HTTP-Error-Handling.md"
}
]
},
{
"text": "Localization",
@ -1173,8 +1182,8 @@
"path": "UI/Angular/Content-Security-Strategy.md"
},
{
"text":"Abp Window Service",
"path":"UI/Angular/Abp-Window-Service.md"
"text": "Abp Window Service",
"path": "UI/Angular/Abp-Window-Service.md"
}
]
},

6
npm/ng-packs/packages/core/src/lib/services/rest.service.ts

@ -20,7 +20,7 @@ export class RestService {
protected externalHttp: ExternalHttpClient,
protected environment: EnvironmentService,
protected httpErrorReporter: HttpErrorReporterService,
) {}
) { }
protected getApiFromStore(apiName: string | undefined): string {
return this.environment.getApiUrl(apiName);
@ -28,7 +28,7 @@ export class RestService {
handleError(err: any): Observable<any> {
this.httpErrorReporter.reportError(err);
return throwError(err);
return throwError(() => err);
}
request<T, R>(
@ -51,7 +51,7 @@ export class RestService {
}),
...options,
} as any)
.pipe(catchError(err => (skipHandleError ? throwError(err) : this.handleError(err))));
.pipe(catchError(err => (skipHandleError ? throwError(() => err) : this.handleError(err))));
}
private getHttpClient(isExternal: boolean) {
return isExternal ? this.externalHttp : this.http;

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

@ -1,7 +1,7 @@
import { inject, Injectable, Injector } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { catchError, filter, switchMap } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
import { HttpErrorReporterService } from '@abp/ng.core';
@ -22,9 +22,7 @@ export class ErrorHandler {
protected readonly routerErrorHandlerService = inject(RouterErrorHandlerService);
protected readonly httpErrorConfig = inject(HTTP_ERROR_CONFIG);
protected readonly customErrorHandlers = inject(CUSTOM_ERROR_HANDLERS);
protected readonly defaultHttpErrorHandler = (_, err: HttpErrorResponse) => throwError(() => err);
protected readonly httpErrorHandler =
inject(HTTP_ERROR_HANDLER, { optional: true }) || this.defaultHttpErrorHandler;
protected readonly httpErrorHandler = inject(HTTP_ERROR_HANDLER, { optional: true });
constructor(protected injector: Injector) {
this.listenToRestError();
@ -42,10 +40,11 @@ export class ErrorHandler {
}
protected executeErrorHandler = (error: HttpErrorResponse) => {
const errHandler = this.httpErrorHandler(this.injector, error);
const isObservable = errHandler instanceof Observable;
if (this.httpErrorHandler) {
return this.httpErrorHandler(this.injector, error);
}
return (isObservable ? errHandler : of(null)).pipe(catchError(err => of(err)));
return of(error);
};
protected sortHttpErrorHandlers(
@ -57,12 +56,12 @@ export class ErrorHandler {
protected handleError(err: unknown) {
if (this.customErrorHandlers && this.customErrorHandlers.length) {
const canHandleService = this.customErrorHandlers
const errorHandlerService = this.customErrorHandlers
.sort(this.sortHttpErrorHandlers)
.find(service => service.canHandle(err));
if (canHandleService) {
canHandleService.execute();
if (errorHandlerService) {
errorHandlerService.execute();
return;
}
}

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

@ -1,5 +1,5 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Type } from '@angular/core';
import { Injector, Type } from '@angular/core';
import { Validation } from '@ngx-validate/core';
import { Observable } from 'rxjs';
import { ConfirmationIcons } from '../tokens/confirmation-icons.token';
@ -20,7 +20,7 @@ export interface HttpErrorConfig {
hideCloseIcon?: boolean;
};
}
export type HttpErrorHandler<T = any> = (httpError: HttpErrorResponse) => Observable<T>;
export type HttpErrorHandler<T = any> = (injector: Injector, httpError: HttpErrorResponse) => Observable<T>;
export type LocaleDirection = 'ltr' | 'rtl';
export interface CustomHttpErrorHandlerService {

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

@ -5,25 +5,25 @@ import { AbpFormatErrorHandlerService } from '../services/abp-format-error-handl
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[] = [
export const DEFAULT_HANDLERS_PROVIDERS: Provider[] = [
{
provide: CUSTOM_ERROR_HANDLERS,
multi: true,
useClass: TenantResolveErrorHandlerService,
useExisting: TenantResolveErrorHandlerService,
},
{
provide: CUSTOM_ERROR_HANDLERS,
multi: true,
useClass: AbpFormatErrorHandlerService,
useExisting: AbpFormatErrorHandlerService,
},
{
provide: CUSTOM_ERROR_HANDLERS,
multi: true,
useClass: StatusCodeErrorHandlerService,
useExisting: StatusCodeErrorHandlerService,
},
{
provide: CUSTOM_ERROR_HANDLERS,
multi: true,
useClass: UnknownStatusCodeErrorHandlerService,
useExisting: UnknownStatusCodeErrorHandlerService,
},
];

4
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 { ERROR_HANDLERS_PROVIDERS, NG_BOOTSTRAP_CONFIG_PROVIDERS } from './providers';
import { DEFAULT_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';
@ -149,7 +149,7 @@ export class ThemeSharedModule {
},
},
tenantNotFoundProvider,
ERROR_HANDLERS_PROVIDERS,
DEFAULT_HANDLERS_PROVIDERS,
],
};
}

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

@ -15,6 +15,9 @@ export function httpErrorConfigFactory(config = {} as HttpErrorConfig) {
export const HTTP_ERROR_CONFIG = new InjectionToken<HttpErrorConfig>('HTTP_ERROR_CONFIG');
/**
@deprecated use **`CUSTOM_ERROR_HANDLERS`** injection token instead of this, see more info https://docs.abp.io/en/abp/latest/UI/Angular/HTTP-Requests
*/
export const HTTP_ERROR_HANDLER = new InjectionToken<HttpErrorHandler>('HTTP_ERROR_HANDLER');
export const CUSTOM_ERROR_HANDLERS = new InjectionToken<CustomHttpErrorHandlerService[]>(

Loading…
Cancel
Save