Why would you want to setup an HTTP Interceptor? Not sure, but I can tell you that I used it to solve a few different problems, out of which I'll discuss in this post:
- Adding an authentication header
- Handling
401
Unauthorized
Bonus: Unit Tests the interceptor (In the next post).
But first, what's an interceptor?
... transform the outgoing request before passing it to the next interceptor in the chain ... An interceptor may transform the response event stream as well, by applying additional RxJS operators on the stream.
Or, in human speak, If you need to make changes or decisions to any request or response - this is where you want to do it.
Add an interceptor to your project
In your app.module.ts
(or what ever you called the root of your project) you should have this code:
import { NgModule } from '@angular/core';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { TokenInterceptor } from './auth/token.interceptor';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [ ... ],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule {}
What's going on here? We're "providing" the app with a tool.
- The
HTTP_INTERCEPTORS
is a symbol - a key - to lock our interceptor into position. It lets the system make sure where we want toprovide
something. - The
TokenInterceptor
is our class we're about to implement. - Finally,
multi: true
means we can provide multiple interceptors, chained, as opposed to overriding each other. In this specific app we only have one interceptor, but if we ever want to add another one, we're ready to go.
The basic interceptor
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
// Note: these are only the initial imports. Add more as needed.
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request);
}
}
We're not really doing anything here, but just to set the basics. Implementing HttpInterceptor
means we need to implement the intercept
function which gets a request
and a handler for continuing the process. Later we'll see what we can do with it.
Add an authentication header
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
request = request.clone({
setHeaders: {
'my-auth-token': `${this.getSessionToken()}`
}
});
return next.handle(request);
}
You can't simply change the request. The request is a readonly
object. But you can clone
it, while overriding specific components. In our scenario, we're using the setHeaders
property to add a token header to the request.
The function getSessionToken
is not provided here and is up to you, as a developer to know how it is stored and how to retrieve it.
Handling backend 401 Unauthorized
Every page of our application makes several XHR calls to the backend. At some point or other, for various reasons, the user's session may expire. Instead of showing the user a pile of error messages - at first sign of trouble (401
) we redirect the user to the login page.
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
request = request.clone({
setHeaders: {
'my-auth-token': `${this.getSessionToken()}`
}
});
return next.handle(request).pipe(
catchError((response: HttpErrorResponse) => {
if (response.status === 401) {
// Do something here
}
return throwError(response);
}
);
}
We've added a pipe after handling the request. This is our way of handling the response. catchError
is an rxjs
function which catches if an error is thrown by the observable. We check the response and "Do something" and then throw the response again. We throw the response so the caller of the request, further down the line, knows that something went wrong and can handle it gracefully, regardless of the interceptor's processing.
Now, why "Do something"? The answer is in a hint I gave earlier - each page makes several calls, and all or some may be throwing 401
errors, and we don't want all of them to "hit" the user at the same time. Enter throttleTime
.
throttleTime
is a sibling of debounce
. While debounce
waits for an action to stop happening, throttleTime
lets the first action go through and then blocks for a given time. Let's setup a subject to "Do something" while "protected" by our throttle.
private throttleLogout = new Subject();
constructor() {
this.throttleLogout.pipe(throttleTime(5000)).subscribe(url => {
this.logout();
});
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
request = request.clone({
setHeaders: {
'my-auth-token': `${this.getSessionToken()}`
}
});
return next.handle(request).pipe(
catchError((response: HttpErrorResponse) => {
if (response.status === 401) {
// Call logout, but throttled!
this.throttleLogout.next();
}
return throwError(response);
}
);
}
In the class constructor, we've initialized a simple void
Subject which is piped through a throttleTime
- once the first 401
is intercepted, the user is logged out and they aren't logged out again (due to 401
) for another five seconds.
In the next post, I'll show you how we wrote unit tests to verify all this functionality.
Top comments (1)
Use em, love em!