Interceptors in Angular are one of the built-in tools for specifically handling HTTP requests at a global application level. Angular provides many built-in tools to help scale out large JavaScript applications and in this post you’ll learn how to use interceptors fully.
Table of contents
Often we want to enforce, or apply, behavior when receiving or sending HTTP requests.
Interceptors are a unique type of Angular Service that we can implement to do exactly that.
Interceptors allow us to “intercept” incoming (or outgoing) HTTP requests using the HttpClient
.
By intercepting an HTTP request, we can modify or change the value of the request.
In this post, we cover three different Interceptor implementations:
- Handling HTTP Headers
- HTTP Response Formatting
- HTTP Error Handling
This post assumes some basic knowledge of the Angular HTTP Client and RxJS Observables. Let’s take a look at the basic API implementation.
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpResponse, HttpRequest, HttpHandler } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class MyInterceptor implements HttpInterceptor {
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(httpRequest);
}
}
To create an Interceptor, we need to implement the HttpInterceptor
interface from @angular/common/http
package.
Every time our application makes an HTTP request using the HttpClient
service, the Interceptor calls the intercept()
method.
When the intercept()
method is called, Angular passes a reference to the httpRequest
object.
With this request, we can inspect it and modify it as necessary.
Once our intercepting is complete, we call next.handle
and return the updated request onto the application.
Interceptors then need to be registered as a multi-provider since there can be multiple interceptors running within an application.
Important note, you must register the provider to the
app.module
for it to properly apply to all application HTTP requests. Interceptors will only intercept requests that are made using theHttpClient
service.
Here’s what that looks like:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { RouterModule, Routes } from '@angular/router';
import { MyInterceptor } from './my.interceptor';
import { AppComponent } from './app.component';
@NgModule({
imports: [BrowserModule, HttpClientModule],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: MyInterceptor, multi: true }
]
})
export class AppModule { }
Next, let’s take a look at our first Interceptor implementation by creating an Interceptor that can modify request headers.
HTTP Header Interceptor
Often we need to return an API key to an authenticated API endpoint via a request Header. Using Angular Interceptors, we can handle this automatically. Let’s make a simple use case of attaching an API header key to each request:
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpResponse, HttpRequest, HttpHandler } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, filter } from 'rxjs/operators';
@Injectable()
export class HeaderInterceptor implements HttpInterceptor {
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const API_KEY = '123456';
return next.handle(httpRequest.clone({ setHeaders: { API_KEY } }));
}
}
On the httpRequest
object, we can call the clone method to modify the request object and return a new copy.
In this example we are attaching the API_KEY
value as a header to every HTTP request httpRequest.clone({ setHeaders: { API_KEY } })
.
Now let’s use the HttpClient
service to make a HTTP get request.
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Component({
selector: 'app-header',
template: `
<h2>Header Example</h2>
<pre>{{ data | json }}</pre>
`
})
export class HeaderComponent implements OnInit {
data: {};
constructor(private httpClient: HttpClient) { }
ngOnInit() {
this.httpClient.get('/assets/header.json').subscribe(data => this.data = data);
}
}
If we look at the dev tools we can see a network request containing our new header API_KEY
with the corresponding value:
Now with each request, we automatically send our API key without having to duplicate the logic throughout our application!
🎯 Important: For security reasons ensure your Interceptor only sends your API key to the APIs that require it by checking the request URL.
Formatting JSON Responses
Often we want to modify the request value that we get back from an API. Sometimes we work with APIs that have formatted data that can make it challenging to work within our application. Using Interceptors, we can format the data and clean it up before it gets to our application logic. Let’s take a look at an example API response.
{
"id": "123",
"metadata": "blah",
"data": {
"users": {
"count": 4,
"list": [
"bob",
"john",
"doe"
]
}
}
}
In this example, the data we want to render in our component is nested deeply within the response object.
The other data is just noise for our app. Using a Interceptor, we can clean up the data, which may be helpful in many places.
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpResponse, HttpRequest, HttpHandler } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, filter } from 'rxjs/operators';
@Injectable()
export class FormatInterceptor implements HttpInterceptor {
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(httpRequest).pipe(
filter(event => event instanceof HttpResponse && httpRequest.url.includes('format')),
map((event: HttpResponse<any>) => event.clone({ body: event.body.data.users.list }))
);
}
}
With the httpRequest
object, we can inspect the URL of the request and determine if it is a request we want to ignore or modify.
If the request is to the format
API endpoint, then we continue and update the response. We also only want to modify the request if the request was a response coming back from our own API:
filter(event => event instanceof HttpResponse && httpRequest.url.includes('format')),
Now that we filter out only the request we care about we can update the response body to be a simple array of the users we want to display:
return next.handle(httpRequest).pipe(
filter(event => event instanceof HttpResponse && httpRequest.url.includes('format')),
map((event: HttpResponse<any>) => event.clone({ body: event.body.data.users.list }))
);
Now in our component, we can subscribe to our data without having to dig into the response object, our API returned.
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Component({
selector: 'app-format',
template: `
<h2>Formated JSON</h2>
<pre>{{ data | json }}</pre>
`
})
export class FormatComponent implements OnInit {
data: {};
constructor(private httpClient: HttpClient) { }
ngOnInit() {
this.httpClient.get('/assets/format.json').subscribe(data => this.data = data);
}
}
Error Handling and Angular Interceptors
Thankfully, we have a few options on how to handle these HTTP Interceptor errors.
We could log errors through the Interceptors, or show a UI notification when something has gone wrong.
In this example, we will add logic that will retry failed API requests:
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpResponse, HttpRequest, HttpHandler, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { retry } from 'rxjs/operators';
@Injectable()
export class RetryInterceptor implements HttpInterceptor {
intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(httpRequest).pipe(retry(2));
}
}
In our request handler, we can use the RxJS retry()
, operator, which allows us to retry failed Observable streams that have thrown errors.
The retry
operator takes a parameter of the number of retries we would like. In our example, we use a parameter of 2
, which totals to three attempts, the first attempt plus two additional retries. If none of the requests succeed, then, the Observable throws an error to the subscriber of the HTTP request Observable.
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Component({
selector: 'app-retry',
template: `<pre>{{ data | json }}</pre>`
})
export class RetryComponent implements OnInit {
data: {};
constructor(private httpClient: HttpClient) { }
ngOnInit() {
this.httpClient.get('https://example.com/404').pipe(
catchError(err => of('there was an error')) // return a Observable with a error message to display
).subscribe(data => this.data = data);
}
}
In our component, when we make a bad request, we still can catch the error using the catchError
operator. This error handler will only be called after the final attempt in our Interceptor has failed.
Above we we can see our three attempts to load the request.
HTTP Interceptors are another helpful tool in our toolbox to manage HTTP requests in our Angular applications and help simplify our code and give us a single point to handle specific HTTP use cases.
Check out the full working demo:
Happy intercepting!