DEV Community

Cover image for Angular HTTP Interceptor
Ayelet Dahan
Ayelet Dahan

Posted on

Angular HTTP Interceptor

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 {}
Enter fullscreen mode Exit fullscreen mode

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 to provide 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

Oldest comments (1)

Collapse
 
leonlafa profile image
Leon Lafayette

Use em, love em!