From 2c37566528661e66fd814c837b47915cda4633ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinan=20=C3=96zt=C3=BCrk?= Date: Fri, 19 Jan 2024 18:36:59 +0300 Subject: [PATCH] update documentation, deprecate HTTP_ERROR_HANDLER injection token --- docs/en/UI/Angular/HTTP-Error-Handling.md | 100 +++++++++++++ docs/en/UI/Angular/HTTP-Requests.md | 140 +----------------- docs/en/docs-nav.json | 15 +- .../src/lib/handlers/error.handler.ts | 12 +- .../lib/providers/error-handlers.provider.ts | 8 +- .../src/lib/tokens/http-error.token.ts | 3 + 6 files changed, 132 insertions(+), 146 deletions(-) create mode 100644 docs/en/UI/Angular/HTTP-Error-Handling.md diff --git a/docs/en/UI/Angular/HTTP-Error-Handling.md b/docs/en/UI/Angular/HTTP-Error-Handling.md new file mode 100644 index 0000000000..8bf2589204 --- /dev/null +++ b/docs/en/UI/Angular/HTTP-Error-Handling.md @@ -0,0 +1,100 @@ +# 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 service can be registered to an injection token named **`CUSTOM_ERROR_HANDLERS`**. ABP has some default error handlers, you can see them [here](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts). + +### How ABP handles errors and How to add new one? + +- First of all it will be better to understand how ABP handles errors. +- 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. You can think this as a z-index property in CSS. +- **`canHandle(error: unknown): boolean :`** Check if the service can handle the error. Returns boolean. +- **`execute(): void :`** If the service can handle the error, then run the execute method. + +**In Summarry** + +- 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; + private 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) + +**Note 1:** If your service cannot handle the error. Then ABP will check the next Error Service. +**Note 2:** If none of the service handle the error. Then basic confirmation message about the error will be shown to the user. +**Note 3:** You can provide more than one service, with CUSTOM_ERROR_HANDLER injection token. +**Note 4:** If you want your custom service to be evaluated (checked) earlier, set the priority variable high. diff --git a/docs/en/UI/Angular/HTTP-Requests.md b/docs/en/UI/Angular/HTTP-Requests.md index b524b4a8ee..c58e35fc51 100644 --- a/docs/en/UI/Angular/HTTP-Requests.md +++ b/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`. 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` 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` 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` 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 = { @@ -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 { 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, 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 `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` can be imported from `rxjs` - -```js -export function handleHttpErrors(injector: Injector, httpError: HttpErrorResponse) { - if (httpError.status === 403) { - // handle 403 errors here - return EMPTY; // put return 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`. - -```js -import { throwError } from 'rxjs'; - -export function handleHttpErrors(injector: Injector, httpError: HttpErrorResponse) { - if (httpError.status === 500) { - // handle 500 errors here - return EMPTY; - } - - // 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) -} -``` - - ### 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,4 @@ 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. diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 8e766c7430..be988b795c 100644 --- a/docs/en/docs-nav.json +++ b/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" } ] }, 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 a3417900a2..168fef1f53 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 @@ -21,7 +21,7 @@ export class ErrorHandler { protected readonly confirmationService = inject(ConfirmationService); protected readonly routerErrorHandlerService = inject(RouterErrorHandlerService); protected readonly httpErrorConfig = inject(HTTP_ERROR_CONFIG); - protected readonly defaultErrorHandlers = inject(CUSTOM_ERROR_HANDLERS); + protected readonly customErrorHandlers = inject(CUSTOM_ERROR_HANDLERS); protected readonly httpErrorHandler = inject(HTTP_ERROR_HANDLER, { optional: true }); constructor(protected injector: Injector) { @@ -43,7 +43,7 @@ export class ErrorHandler { if (this.httpErrorHandler) { return this.httpErrorHandler(this.injector, error); } - + return of(error); }; @@ -55,17 +55,17 @@ export class ErrorHandler { } protected handleError(err: unknown) { - if (this.defaultErrorHandlers && this.defaultErrorHandlers.length) { - const errorHandlerService = this.defaultErrorHandlers + if (this.customErrorHandlers && this.customErrorHandlers.length) { + const errorHandlerService = this.customErrorHandlers .sort(this.sortHttpErrorHandlers) .find(service => service.canHandle(err)); - + if (errorHandlerService) { errorHandlerService.execute(); return; } } - + this.showError().subscribe(); } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts b/npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts index 61a2a25fcd..d456c82912 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/providers/error-handlers.provider.ts @@ -9,21 +9,21 @@ 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, }, ]; 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 a2784f4332..64245680db 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 @@ -15,6 +15,9 @@ export function httpErrorConfigFactory(config = {} as HttpErrorConfig) { export const HTTP_ERROR_CONFIG = new InjectionToken('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('HTTP_ERROR_HANDLER'); export const CUSTOM_ERROR_HANDLERS = new InjectionToken(