Creating a Robust Angular HttpClient Interceptor
Angular HttpClient interceptor HTTP requests responses best practices tips middleware error handling testing

José Matos

26 Mar 2023

Creating a Robust Angular HttpClient Interceptor

    Creating a Robust Angular HttpClient Interceptor

    When it comes to consuming HTTP resources in our Angular app, HttpClient is the go-to solution. It provides us with an easier way to make HTTP requests and, most importantly, it returns Observable responses that work incredibly well with Angular’s async pipe.

    If we want to add some custom logic or headers to every HTTP request in our app, HttpClient interceptors come to the rescue. However, creating a robust interceptor can be challenging, and a there are a few potential pitfalls to avoid. In this article, we’ll explore some best practices and tips for creating a robust Angular HttpClient interceptor.

    What is an Angular HttpClient Interceptor?

    Before we dive into creating a robust interceptor, let’s first define what an interceptor is. An interceptor in Angular is simply a service that wraps around the HttpClient and allows us to intercept outgoing HTTP requests and incoming responses before they reach the endpoint or the client. In other words, an interceptor is middleware that sits between the client and the server.

    Interceptors provide us with great flexibility for manipulating the request and response objects, adding headers, modifying the body, or even adding error handling — all without modifying the actual client request.

    Creating an Angular HttpClient Interceptor

    Creating an interceptor service in Angular is straightforward. We just need to implement the HttpInterceptor interface and define the intercept method. Here’s an example:

    
    import { Injectable } from '@angular/core';
    import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';
    
    @Injectable()
    export class MyInterceptor implements HttpInterceptor {
      intercept(req: HttpRequest<any>, next: HttpHandler){
        // add some logic here
        const modifiedReq = req.clone({
          headers: req.headers.set('X-Custom-Header', 'MyCustomHeaderValue')
        });
        return next.handle(modifiedReq); // pass on the request to the next handler
      }
    }
    

    Once our interceptor is defined, we simply need to provide it in the app module using the HTTP_INTERCEPTORS injection token:

    
    import { NgModule } from '@angular/core';
    import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
    import { MyInterceptor } from './my-interceptor';
    
    @NgModule({
      declarations: [],
      imports: [
        HttpClientModule,
      ],
      providers: [
        { provide: HTTP_INTERCEPTORS, useClass: MyInterceptor, multi: true },
      ],
    })
    export class AppModule { }
    

    Notice that we are using the multi option when providing the interceptor. This ensures that we are not overwriting any existing interceptors that might be defined.

    Best Practices for Creating a Robust Interceptor

    Now that we have created our interceptor, let’s explore some best practices and tips for making it robust.

    1. Don’t Break the Chain

    When we are chaining multiple interceptors, it’s important to pass the request object to the next interceptor via the next.handle() method. Failing to do so would break the chain and cause the request to never reach the endpoint. Here’s an example:

    
    export class MyFirstInterceptor implements HttpInterceptor {
      intercept(req: HttpRequest<any>, next: HttpHandler){
        const modifiedReq = req.clone({
          headers: req.headers.set('X-Custom-Header', 'MyCustomHeaderValue')
        });
        // don't forget to pass the request to the next handler
        return next.handle(modifiedReq);
      }
    }
    
    export class MySecondInterceptor implements HttpInterceptor {
      intercept(req: HttpRequest<any>, next: HttpHandler){
        const modifiedReq = req.clone({
          headers: req.headers.set('X-Custom-Header-2', 'MyCustomHeaderValue2')
        });
        // don't break the chain!
        return next.handle(modifiedReq);
      }
    }
    

    Always make sure that you return the result of next.handle() to pass the request to the next interceptor in the chain.

    2. Keep it Stateless

    Interceptors should be stateless. This means that they should not modify the request object in a way that other interceptors would rely on. Modifying the request object is allowed, as long as we don’t break the chain, but we should avoid adding stateful logic to our interceptors.

    3. Handle Errors Gracefully

    When working with interceptors, we should also take into account error handling. It’s important to handle any errors gracefully and provide meaningful error messages to the user.

    The HttpErrorResponse object returned by HttpClient in case of errors contains useful information, such as the error message and status code. Here’s an example of error handling in an interceptor:

    
    export class ErrorHandlerInterceptor implements HttpInterceptor {
      intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
        return next.handle(request).pipe(
          catchError((error: HttpErrorResponse) => {
            if (error.status === 401) {
              // handle unauthorized error
            } else if (error.status === 403) {
              // handle forbidden error
            } else if (error.status === 404) {
              // handle not found error
            } else {
              // handle other error
            }
            return throwError(error);
          }),
        );
      }
    }
    

    We are using the catchError() operator from rxjs to catch any errors thrown by the next handler. We can then handle the error based on its status code and return a meaningful error message to the user.

    4. Test Your Interceptors

    Don’t forget to test your interceptors thoroughly! Interceptors should be tested just like any other service in your Angular app. Here’s a simple test that checks whether our interceptor adds a custom header to the request:

    
    import { TestBed, async, inject } from '@angular/core/testing';
    import {
      HttpClientTestingModule,
      HttpTestingController,
    } from '@angular/common/http/testing';
    import { MyInterceptor } from './my-interceptor';
    
    describe('MyInterceptor', () => {
      beforeEach(() => {
        TestBed.configureTestingModule({
          imports: [HttpClientTestingModule],
          providers: [MyInterceptor],
        });
      });
    
      it(
        'should add custom header to the request',
        async(
          inject(
            [HttpTestingController, MyInterceptor],
            (httpMock: HttpTestingController, interceptor: MyInterceptor) => {
              interceptor.intercept(
                // create a mock request
                new HttpRequest('GET', 'https://example.com'),
                // mock the next handler
                {
                  handle: req => {
                    // check if interceptor added the custom header
                    expect(req.headers.get('X-Custom-Header')).toEqual(
                      'MyCustomHeaderValue',
                    );
                    return EMPTY;
                  },
                },
              );
            },
          ),
        ),
      );
    });
    

    We are using the HttpClientTestingModule and the HttpTestingController to mock the HTTP requests and responses. Then, we create a mock request and verify that our interceptor has added the custom header to the request.

    Conclusion

    HttpClient interceptors are a powerful tool in Angular for manipulating HTTP requests and responses. Creating a robust interceptor is not hard, but it does require following some best practices and testing thoroughly. By keeping our interceptors stateless, not breaking the chain, handling errors gracefully, and testing them thoroughly, we can create robust interceptors that work seamlessly with our Angular app.

    © 2023 Designed & Developed by José Matos.