Interacting efficiently with a RESTful service with Angular2 and RXJS (Part 3)

In the previous part of the article, we dealt with the way to manage data from a RESTful service and to link requests with form elements. We now tackle global issues like request interception, error handling, security, and retry.

Intercepting requests

The request interception feature isn’t provided out of the box by Angular2. That said, the framework allows you to override classes gotten from dependency injection. The Http class is responsible from leveraging the underlying XMLHttpObject object to execute HTTP requests.

This class is the one to extend to intercept request calls. The following snippet describes how to implement such class to detect events: before the request, after request, and on error. For this, we leverage the catch and finally operators of observables.

@Injectable()
export class CustomHttp extends Http {
  constructor(backend: ConnectionBackend, defaultOptions: RequestOptions) {
    super(backend, defaultOptions);
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    console.log('Before the request...');
    return super.request(url, options)
        .catch((err) => {
          console.log('On received an error...');
          return Observable.throw(err);
        })
        .finally(() => {
          console.log(After the request...');
        });
  }

  get(url: string, options?: RequestOptionsArgs): Observable<Response> {
    console.log('Before the request...');
    return super.get(url, options)
        .catch((err) => {
          console.log('On received an error...');
          return Observable.throw(err);
        })
        .finally(() => {
          console.log(After the request...');
        });
  }
}

Now we implemented the CustomHttp class, we need to configure its provider when bootstrapping our application. This can be done using the provide function with the useFactoryattribute to keep the hand on the way to instantiate it and provide it the right dependencies.

bootstrap(AppComponent, [
  HTTP_PROVIDERS,
  provide(Http, {
    useFactory: (backend: XHRBackend, defaultOptions: RequestOptions) => {
      return new CustomHttp(backend, defaultOptions);
    },
    deps: [ XHRBackend, RequestOptions ]
  })
]);

Be careful to configure the provider of the CustomHttp class after specifying HTTP_PROVIDERS.

This class will be used in the following as foundations to implement generic features like error handling, security and retries under the hood and without any updates in parts of the application that uses the http object. Let’s start by error handling.

Handling errors

Sure we could handle errors for each HTTP call but this could be inconvenient. A better approach consists of leveraging our CustomHttp class. Using the catch operator we can intercept errors. We need then to notify the other parts of the application. For example, the component that is responsible for displaying them.

For this, we need to create a dedicated service ErrorNotifierService that will include an observable and its associated observer. Based on this service, we will be able to notify when errors occur and to be notified.

export class ErrorNotifierService {
  private errorObservable:Observable<any>;
  private errorObserver:Observer<any>;

  constructor() {
    this.errorObservable = Observable.create((observer:Observer) => {
      this.errorObserver = observer;
    }).share();
  }

  notifyError(error:any) {
    this.errorObserver.next(error);
  }

  onError(callback:(err:any) => void) {
    this.errorObservable.subscribe(callback);
  }
}

Be careful to register this service when bootstrapping your application to make it shared by the whole application.

bootstrap(AppComponent, [
  (...)
  ErrorNotifierService
]);

This can be injected in the CustomHttp class. When an error is caught, the notifyError method can be called. All components that registered a callback using the onError method, we will be notified and display messages accordingly.

@Injectable()
export class CustomHttp extends Http {
  constructor(backend: ConnectionBackend,
            defaultOptions: RequestOptions,
            private errorService:ErrorNotifierService) {
    super(backend, defaultOptions);
  }

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    console.log('Before the request...');
    return super.request(url, options)
        .catch((err) => {
          this.errorService.notifyError(err);
        })
        .finally(() => {
          console.log(After the request...');
        });
  }

  (...)
}

Don’t forget to update the way we configured our CustomHttp class for dependency injection:

bootstrap(AppComponent, [
  HTTP_PROVIDERS,
  provide(Http, {
    useFactory: (backend: XHRBackend, defaultOptions: RequestOptions,
                         errorService:ErrorNotifierService) => {
      return new CustomHttp(backend, defaultOptions, errorService);
    },
    deps: [ XHRBackend, RequestOptions, ErrorNotifierService ]
  }),
  ErrorNotifierService
]);

This approach is a bit too global. In some cases, we don’t need to intercept the error but let the error be handled by the code that triggers the request. It’s particularly the case for asynchronous validators of forms and partially when submitting forms. In fact, it’s rather linked to the HTTP status code of the response. The 400 and 422 ones must be handled separately to add some user-friendly messages within forms. For example along with a specific field.

In this case, we need to throw the intercepted error, as described below:

@Injectable()
export class CustomHttp extends Http {
  (...)

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    console.log('Before the request...');
    return super.request(url, options)
        .catch((err) => {
          if (err.status === 400 || err.status === 422) {
            return Observable.throw(err);
          } else {
            this.errorService.notifyError(err);
          }
        })
        .finally(() => {
          console.log(After the request...');
        });
  }

  (...)
}

Handling security

As you can see, we didn’t handle security when executing HTTP requests. That said we don’t want to impact all this code by adding it. Using the class that extends the Http one, we will be able to such processing under the hood without any impact on the existing code.

There are several parts where security can apply especially if the routing feature of Angular is used. For example, we could extend the RouterOutlet directive to check if the user is authenticated before activating secured routes. We could also extend the RouterLink one to check the current roles of the user to display or hide links to secure routes. All these aspects would contribute to improving the user experience.

In this section, we will only focus on securing HTTP calls transparently, i.e. with no impact on the existing code that uses the Http class. We will of course leverage the request interception mechanism previously described.

We can even go further by displaying a dialog to invite the user to fill his credentials. Under the hood, it can be basic authentication or token-based security with tokens that need to be revalidated automatically using a refresh token.

For the basic authentication, it only corresponds to setting the Authorization header based on the credentials the user provides when authenticating. In the case, using the merge method of the request options is enough:

merge(options?:RequestOptionsArgs):RequestOptions {
  (...)
  var credentials = this.securityService.getCredentials();
  if (credentials) {
    let headers = options.headers | {};
    headers['Authorization] = 'Basic ' +
          btoa(credentials.username + ':' + credentials.password);
    options.headers = headers;
  }
  return super.merge(options);
}

In the case of tokens, this must be done within our custom Http class since we need to eventually chain requests using observables and their operators. If we detected that the current token expired, we need to execute the request to refresh this token. When we receive the response, we need to return the observable for the initial request using the flatMap operator or throw an error.

request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
  if (this.securityService.hasTokenExpired()) {
    return this.securityService
             .refreshAuthenticationObservable()
             .flatMap((authenticationResult:AuthenticationResult) => {
               if (authenticationResult.authenticated) {
                 this.securityService.setAuthorizationHeader(request.headers);
                 return super.request(url, request);
               } else {
                 return Observable.throw(new Error('Can't refresh the token'));
               }
    });
  } else {
    return super.request(url, options);
  }
}

The last thing missing to our application is the ability retry requests in failure before returning the failure to the application.

Retry support

Before considering a request as failed, we could consider implementing a retry mechanism. This feature is supported out of the box by observables thanks to the retry operator. Let’s add some retries when calling an HTTP request, for example, within the getBookKinds method.

getBookKinds(): Observable<BookKind[]> {
  return this.http.get('/bookkinds')
                  .retry(4)
                  .map(res => res.map());
}

This approach is much too basic since retries are immediately executed without waiting for a delay. A better one consists of waiting for a bit before retrying and abort after a given amount of time. Observables allow to mix retryWhendelay and timeout operators to achieve this, as described in the following snippet:

getBookKinds(): Observable<BookKind[]> {
  return this.http.get('/bookkinds')
                  .retryWhen(error => error.delay(500))
                  .timeout(2000, new Error('delay exceeded'))
                  .map(res => res.map());
}

Because such a feature is generic, we can implement it within our CustomHttp class as described below:

@Injectable()
export class CustomHttp extends Http {
  (...)

  request(url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
    return super.request(url, options)
        .retryWhen(error => error.delay(500))
        .timeout(2000, new Error('delay exceeded'));
  }

  (...)
}

You simplify the code above for clarity by removing processing for security and handling previously described.

Conclusion

In this article, we discussed how to interact with a RESTful service from an Angular 2 application. We deal with the HTTP support provided by the framework and show how Reactive Programming can fit at this level and what it can provide.

We also describe how to implement concrete use cases to control the way requests are executed, to get data from several requests, error handling, security, and retries. All these features contribute to making the application most robust.

We made an effort on the design to leverage the best features and mechanisms of Angular 2. The idea is to add global features with the least impact on the existing code of the application.

The source code is available in the following Github repository: https://github.com/restlet/restlet-samples-angular2-rxjs.

原文鏈接: http://restlet.com/blog/2016/04/18/interacting-efficiently-with-a-restful-service-with-angular2-and-rxjs-part-3/

發佈了1 篇原創文章 · 獲贊 11 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章