A couple of days ago, I published an article on HTTP Client that described the basics of integrating an Angular application with a server-side API or a Web server.
The tutorial addressed the most common scenarios that developers come across while making a HTTP call from an Angular application. Some of these scenarios were:
- making HTTP calls over GET/POST/PUT/DELETE methods.
- error handling scenarios and
- solutions for separating presentation logic from service API integration details.
To learn more, read the tutorial over here: www.dotnetcurry.com/angularjs/1438/http-client-angular
In this tutorial, we will cover some advanced scenarios. Certain requirements like Security, Logging, Auditing etc., need better flexibility and control on data exchange over HTTP. Often the requirement is overarching and needs a central piece of code addressing it, and this code needs to be unobtrusive and easy to integrate.
Let's begin by looking at ways to access complete HTTP request/response.
Are you keeping up with new developer technologies? Advance your IT career with our Free Developer magazines covering Angular, React, .NET Core, MVC, Azure and more. Subscribe to this magazine for FREE and download all previous, current and upcoming editions.
HTTP Client in Angular - Advanced Scenarios
Add headers to the request
Many a times, web servers need additional headers in the request.
Additional headers could include authorization tokens, content type information, session identifiers etc. HTTP Client provides an easy way to add headers to the request.
Consider the following code which adds an authorization header to a GET HTTP call.
return this.client.get<Traveller>(`${DATA_ACCESS_PREFIX}`,{
headers: new HttpHeaders({
"authorization": aUserIdentifier
})
})
The GET call has an additional object as a parameter. The object includes field “headers” of type HttpHeaders.
We added a new header “authorization” with a value that identifies uniquely for the server. It might perform authorization based on the provided identifier.
Figure 1 shows the network tab on Google Chrome with the additional header in the request.
Figure 1: Additional header in the request
Add parameters to the request
Adding request parameters is another way to send additional information to the server. Key value pairs can be added to the request to send additional data. It shows as a querystring on the URL and is commonly used with GET calls.
Consider the following code and Figure -2.
return this.client.get<Traveller>(`${DATA_ACCESS_PREFIX}`,{
params: {
"travellerId": "1001"
}
})
Figure 2: Adding request parameters
Similar to the code we just saw, the parameter could also be a filter condition. Over here we are providing traveller id to be searched.
Access complete response object
In the previous tutorial, we retrieved typed data objects over the network. The JSON response structure was predefined. The Angular application accessed the data that fit into a predefined TypeScript interface structure.
However, there could be a scenario where we not only need to access response body (JSON data), we also need to access response headers.
Observe Response
HttpClient object allows accessing complete response, including headers. In the browser, response body is a JSON object, which can be copied to a typescript interface or class type.
Response headers are key/value pairs. Consider the following code that accesses complete response object.
--- data-access-Service.ts---
getTravellersFullResponse():Observable<HttpResponse<Traveller>>{
return this.client.get<Traveller>(`${DATA_ACCESS_PREFIX}`,{
observe: "response"
});
}
Notice the return type of the function is Observable of generic type HttpResponse, which in-turn has a generic type parameter Traveller. The Traveller object refers to the HTTP response body.
Consider the following code snippet calling the data-access-service and using the response object.
--- traveller-list.component.ts ---
travellers: Array<Traveller> = [];
this.dataAccess.getTravellersFullResponse()
.subscribe(
(response) => {
this.travellers = this.travellers.concat(response.body);
response.headers.keys().map( (key) => console.log(`${key}: ${response.headers.get(key)}`));
}
);
As mentioned in the paragraph above, based on the generic parameter, HttpResponse is expected to return body of type Traveller (HtpResponse<Traveller>).
In the component code above, instead of returned value from the service function, use body to bind to the UI. Notice, travellers, a class level object in the component. This object is available for binding to HTML controls in the template. It is an array of type Traveller.
Headers are key/value pairs. Hence loop through headers and retrieve values based on the key. The sample console logs the headers. Consider Figure 3.
Figure 3: Header log
Observe HttpEvent
In the prior section, when response was observed, it returned a HttpResponse object. We had access to the whole response, instead of just the body. This approach has more details about the response that is receiving a JSON object.
However, if we need an even better control and need to access individual events or steps while making a HTTP call, observe HttpEvent. It works at a much lower level. It exposes events that capture progress of both the request and response.
Note: In a forthcoming section, all the six events with HttpEvent are listed.
To get started with observing events, consider the following code.
--- data-access-Service.ts---
getTravellersResponseByEvents(){
return this.client.get(`${DATA_ACCESS_PREFIX}`,{
observe: "events"
});
}
Notice, the value for observation is events. The getTravellersResponseByEvents returns HttpEvents of generic type object (HttpEvents<object>).
Consider the following code. The subscribe function is called at every stage of the HTTP call. Progress information is much more detailed. On subscribe, the sample has two out of six events. The if condition checks for Http Event Type received. In the sample, we update the status message when a request is sent, a response is received and is ready to use.
--- traveller-list.component.ts ---
this.dataAccess.getTravellersResponseByEvents()
.subscribe( (result) => {
if(result.type === HttpEventType.Sent){
console.log("request sent");
this.messages.push("request sent"); // update status message
}else if(result.type === HttpEventType.Response){
console.log("response obtained");
this.messages.push("response ready"); // update status message
this.travellers = this.travellers.concat(result.body as Array<Traveller>) // object is ready to use.
}
});
See the following code sninppet for HTML template that shows status messages.
In the TypeScript component code we just saw, the messages object holds status messages. At every stage the array is pushed with new messages. The template also shows list of travellers. This list is updated to an object named travellers (in the component). It is done once the result type is HttpEventType.Response.
--- traveller-list.component.html ---
<div *ngFor="let msg of messages">
<div> {{ msg }}</div>
</div>
<hr />
<div *ngFor="let traveller of travellers">
<div> {{traveller.lastName}}, {{traveller.firstName}} is {{traveller.age}} years old.</div>
<div> City: {{traveller.city}}. Country: {{traveller.country}}</div>
<div><button (click)="deleteTraveller(traveller.id)">Delete</button></div>
<br />
</div>
See Figure 4 for the result.
Figure 4: Status Messages
Following is the complete list of HTTP event types available.
- HttpEventType.Sent (numeration value 0) - Request sent
- HttpEventType.UploadProgress (Enumeration value 1) - Request upload in progress
- HttpEventType.ResponseHeader (Enumeration value 2) - Response headers received
- HttpEventType.DownloadProgress (Enumeration value 3) - Response download in progress
- HttpEventType.Response (Enumeration value 4) - Response ready to use
- HttpEventType.User (Enumeration value 5) - For a custom event.
Show non-JSON data
There are always scenarios to download files, BLOB (binary large object) or text from the remote server. An Angular application needs to support retrieving such content.
Consider the following sample that retrieves text content and shows it on the page. Notice responseType is set as text, anticipating text response (instead of JSON).
--- data-access-Service.ts---
getText(){
return this.client.get(`${BLOBL_ACCESS_PREFIX}/1.txt`,{
responseType: "text"
});
}
A Component calls and consumes the text response. It sets in on a string variable and shows it on the template.
--- show-text.component.ts ---
downloadText(){
this.dataAccess.getText()
.subscribe((data) => {
this.textContent = data;
}, () => console.log("error"));
}
--- show-text.component.html ---
<button (click)="downloadText()">Download Text </button>
<div>{{textContent}}</div>
We may also download a PDF document with a similar approach. Consider the following code where we will use responseType blob instead of text.
--- data-access-Service.ts---
getDocument(){
return this.client.get(`${BLOBL_ACCESS_PREFIX}/1.pdf`,{
responseType: "blob"
});
}
The component will use the data slightly different. Will use the BLOB returned from the service and open it on the window to show the file/content.
--- show-pdf.component.ts ---
downloadDocument(){
this.dataAccess.getDocument()
.subscribe((data) => {
console.log(data);
let url = window.URL.createObjectURL(data);
window.open(url); // The downloaded file is open with the help of a browser window.
}, () => console.log("error"));
}
Interceptors
UI applications commonly need altering of request and response before it’s sent on the wire and right after receiving. It could be for various reasons including logging request/responses, adding common authorization headers that the server might need, caching and so on.
Such activities are preferably carried out at a central location. Interceptors are perfect for this requirement.
In Angular, a series of interceptors can be created, which pass the request from one interceptor to the other. They are ordered and the request is passed in sequence. However, response is received in exact reverse order to the requests.
For example, consider the following interceptor
- Caching Interceptor – stores certain details from a response in local storage or session/storage.
- Authorization Interceptor - Adds required header to all requests matching a pattern.
- Logging Interceptor - Logs data certain type of data being exchanged.
Let us consider that the Caching Interceptor is run first. It does required processing on the request and invokes authorization interceptor if the request needs to go to the server.
Say authorization headers are added and passes the request for logging. The logging interceptor processes it and allows the actual network call. When the response is received, it is first received by the Logging Interceptor, which passes it to Authorization Interceptor, which passes it to the Cache Interceptor that finally sends it back to the UI application and components.
Figure 5: Interceptors in action
Create the first interceptor
To create an interceptor, create a service that implements HttpInterceptor (in module @angular/common/http). To create a service with Angular CLI, run the following command,
ng g service services/logging-interceptor
Import and implement the HttpInterceptor. The interface needs a definition for the function intercept. Consider the following code.
The interceptor doesn’t do anything useful yet. It just logs a static message to the browser console.
@Injectable()
export class LoggingInterceptorService implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>{
console.log("request/response intercepted");
return next.handle(req);
}
constructor() { }
}
The Request is passed in as a parameter to the interceptor (or the intercept function). This is how interceptor has access to the request. Considering the function intercept returns an Observable, we can access the response every time the observable is resolved or errored out.
Notice, the intercept function returns an Observable of generic type HttpEvent. That means, interceptor has access to all the six events that occur with a HTTP call. Refer to the Observe Http Event section above for details on the events.
After an interceptor does its job with the request, it needs to call the handle function next (which is an object HttpHandler). This process invokes next interceptor in the chain.
Provide the Interceptor
Interceptor needs to be provided before the service making the Http call. The Injector uses all interceptors that are provided at the time of injecting a service (that makes HTTP calls).
If an interceptor is provided after the service that uses HttpClient instance, it would be ignored.
Note: Providing a service makes it available for the injector. A service is added to providers array while creating an Angular module or a component. It can be provided with a token similar to the following example. If provided without a token, it will be auto created.
In this sample, let’s provide the interceptor in the AppModule. Begin by importing token for the interceptor HTTP_INTERCEPTORS ( from @angular/common/http). Import the service LoggingInterceptor as well.
Provide the interceptor as shown the following sample:
@NgModule({
declarations: [
...
],
imports: [
...
],
providers: [
DataAccessService,
{provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptorService, multi:true}
],
bootstrap: [AppComponent]
})
export class AppModule { }
Please note, the field multi indicates whether or not the token being provided can be used for multiple services. With the value true, even though in this example we are just using one service, we can provide many services with the same token. See figure-6 for the result as we make HTTP calls.
Figure 6: HTTP Calls
Access response in interceptor
As mentioned above, the intercept function returns an Observable of type HttpEvent. Once subscribed (in a function that consumes response - do not subscribe in interceptor), the Http call is invoked. We can use the operator tap, imported from rxjs/operators to access the success or failure response.
Consider the following code:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>{
console.log("request/response intercepted");
return next.handle(req)
.pipe(
tap(
(result: HttpEvent<any>) => console.log("response intercepted", result),
(error: HttpErrorResponse) => console.error("error response intercepted", error)
)
);
}
Please note, the parameter to the success function is of type HttpEvent of a generic type. And for the error handler, the parameter is of type HttpErrorResponse.
Figure-7 shows success and error response.
Figure 7: Success Error Response
Conclusion
Most Angular applications’ server-side API integration can be achieved with basic HttpClient API.
The service is ready-made and provides an easy way to integrate with RESTful services. However, there are times where the developer needs deeper control while making HTTP calls. Concepts discussed in this article are intended to address such scenarios.
We began by providing the ability to access request and response headers. It allows developers to customize the HTTP call. We later addressed writing such custom code in a reusable manner.
By creating an interceptor, we can write code that accesses and modifies headers, massages data from a central location. Without interceptors, developers would need to modify each service function to do custom editing/reading additional details.
References
Angular documentation for HttpClient -https://angular.io/guide/http
This article was technically reviewed by Ravi Kiran.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
V Keerti Kotaru is an author and a blogger. He has authored two books on Angular and Material Design. He was a Microsoft MVP (2016-2019) and a frequent contributor to the developer community. Subscribe to V Keerti Kotaru's thoughts at his
Twitter account. Checkout his past blogs, books and contributions at
kvkirthy.github.io/showcase.