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 useFactory
attribute 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 retryWhen
, delay
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/