Insider’s Guide into Interceptors and HttpClient mechanics in Angular The original technical blog post was written and published by Max Koretskyi, who currently works as Developer Advocate at AG-Grid. Development ambassadors are responsible for ensuring that their companies listen to the community and communicate their actions and goals to the community. They serve as a link between the community and the company. Translator: Ice Panpan; Proofreader: DreamDevil00

As you probably know, Angular introduced a powerful new HttpClient in version 4.3. One of its primary functions is request interception — the ability to declare an interceptor that lies between the application and the back end. The interceptor documentation is well written, showing how to write and register an interceptor. In this article, I’ll delve into the internals of the HttpClient service, specifically interceptors. I believe this knowledge is necessary for further use of this feature. By the end of this article, you’ll be able to easily understand the workflow of tools like caching and be comfortable implementing complex request/response operation schemes.

First, we’ll use the method described in the documentation to register two interceptors to add custom request headers to the request. We will then implement a custom middleware chain instead of using the mechanism Angular defines. Finally, we’ll see how HttpClient’s request methods build an Observable of HttpEvents type and meet the immutability requirements.

As with most of my articles, we’ll learn more by working with examples.

The sample application

First, let’s implement two simple interceptors, each of which adds a request header to an outgoing request using the methods described in the documentation. For each interceptor, we declare a class that implements the Intercept method. In this method, we modify the request by adding the request headers for custom-header-1 and custom-header-2:

@Injectable(a)export class I1 implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any> > {const modified = req.clone({setHeaders: {'Custom-Header-1': '1'}});
        returnnext.handle(modified); }}@Injectable(a)export class I2 implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any> > {const modified = req.clone({setHeaders: {'Custom-Header-2': '2'}});
        returnnext.handle(modified); }}Copy the code

As you can see, each interceptor takes the next handler as its second argument. We need to pass control to the next interceptor in the middleware chain by calling this function. We’ll soon find out what happens when we call next. Handle and why sometimes you don’t need to call this function. Also, if you’ve been wondering why you need to call the Clone () method on a request, you’ll soon get the answer.

Once interceptors are implemented, we need to register them with the HTTP_INTERCEPTORS token:

@NgModule({
    imports: [BrowserModule, HttpClientModule],
    declarations: [AppComponent],
    providers: [
        {
            provide: HTTP_INTERCEPTORS,
            useClass: I1,
            multi: true
        },
        {
            provide: HTTP_INTERCEPTORS,
            useClass: I2,
            multi: true
        }
    ],
    bootstrap: [AppComponent]
})
export class AppModule {}
Copy the code

We then execute a simple request to check if our custom header was added successfully:

@Component({
    selector: 'my-app',
    template: ` 
      

Response

{{response|async|json}}
`
,})export class AppComponent { response: Observable<any>; constructor(private http: HttpClient) {} request() { const url = 'https://jsonplaceholder.typicode.com/posts/1'; this.response = this.http.get(url, {observe: 'response'}); }}Copy the code

If we have done everything correctly, when we check the Network TAB, we can see our custom request hair sent to the server:

That’s easy. You can find this basic example on Stackblitz. Now it’s time to move on to something more interesting.

Implement custom middleware chains

Our task is to manually integrate interceptors into the logic that handles requests without using the methods provided by HttpClient. At the same time, we’ll build a handler chain, just like Angular does internally.

Handle the request

In modern browsers, AJAX functionality is implemented using the XmlHttpRequest or Fetch API. In addition, there are frequently used JSONP techniques that lead to unexpected results related to change detection. Angular needs a service that uses one of these methods to make requests to the server. This service is called backend in the HttpClient documentation, for example:

In an interceptor, next always represents the next interceptor in the chain, if any, or the final backend if there are no more interceptors

In the interceptor,nextAlways represents the next interceptor (if any) in the chain, and the final backend if there are no more interceptors

In the HttpClient module provided by Angular, there are two ways to implement this service — HttpXhrBackend using the XmlHttpRequest API and JsonpClientBackend using the JSONP technology. HttpXhrBackend is used by default in HttpClient.

Angular defines an abstract concept called HTTP (Request) Handler that handles requests. The middleware chain that handles the request is made up of HTTP Handlers, which pass the request to the next handler in the chain until one of them returns an Observable stream. The interface to the handler is defined by the abstract class HttpHandler:

export abstract class HttpHandler {
    abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any> >; }Copy the code

Backend services such as HttpXhrBackend can process requests by issuing network requests, so it is an example of an HTTP handler. Communicating with backend services to process requests is the most common form of processing, but not the only one. Another common example of request processing is serving requests from the local cache rather than sending them to the server. Therefore, any service that can handle requests should implement the Handle method, which returns an Observable of type HTTP Events, such as HttpProgressEvent, based on the function signature. HttpHeaderResponse or HttpResponse. Therefore, if we want to provide some custom request processing logic, we need to create a service that implements the HttpHandler interface.

Backend is used as an HTTP handler

The HttpClient service injects a global HTTP handler under the HttpHandler token in the DI container. The request is then made by calling its Handle method:

export class HttpClient {
    constructor(private handler: HttpHandler) {} request(...) : Observable<any> {...const events$: Observable<HttpEvent<any>> = 
            of(req).pipe(concatMap((req: HttpRequest<any>) = > this.handler.handle(req))); . }}Copy the code

By default, the global HTTP handler is HttpXhrBackend backend. It is registered under the HttpBackend token in the injector.

@NgModule({
    providers: [
        HttpXhrBackend,
        { provide: HttpBackend, useExisting: HttpXhrBackend } 
    ]
})
export class HttpClientModule {}
Copy the code

As you might guess, HttpXhrBackend implements the HttpHandler interface:

export abstract class HttpHandler {
    abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any> >; }export abstract class HttpBackend implements HttpHandler {
    abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any> >; }export class HttpXhrBackend implements HttpBackend {
    handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {}
}
Copy the code

Since the default XHR Backend is registered under the HttpBackend token, we can inject and replace the usage of HttpClient for making requests ourselves. We replace the following version that uses HttpClient:

export class AppComponent {
    response: Observable<any>;
    constructor(private http: HttpClient) {}

    request() {
        const url = 'https://jsonplaceholder.typicode.com/posts/1';
        this.response = this.http.get(url, {observe: 'body'}); }}Copy the code

Let’s use the default XHR Backend directly, as shown below:

export class AppComponent {
    response: Observable<any>;
    constructor(private backend: HttpXhrBackend) {}

    request() {
        const req = new HttpRequest('GET'.'https://jsonplaceholder.typicode.com/posts/1');
        this.response = this.backend.handle(req); }}Copy the code

Here’s an example. There are a few things to note in the example. First, we need to manually build HttpRequest. Second, because the Backend handler returns the HTTP Events stream, you will see different objects flash by on the screen, eventually rendering the entire HTTP response object.

Adding interceptors

We have tried to use Backend directly, but since we are not running an interceptor, the request header has not been added to the request. An interceptor contains the logic to process the request, but to be used with HttpClient, it needs to be wrapped into a service that implements the HttpHandler interface. We implement this service by executing an interceptor and passing a reference to the next handler in the chain to the interceptor. The interceptor can then trigger the next handler, which is usually backend. To do this, each custom handler saves a reference to the next handler in the chain and passes it along with the request to the next interceptor. Here’s what we want:

This wrapper handler already exists in Angular and is called HttpInterceptorHandler. Let’s use it to wrap one of our interceptors. Unfortunately, Angular doesn’t export it as a public API, so we have to copy the basic implementation from source:

export class HttpInterceptorHandler implements HttpHandler {
    constructor(private next: HttpHandler, private interceptor: HttpInterceptor) {}

    handle(req: HttpRequest<any>): Observable<HttpEvent<any> > {// execute an interceptor and pass the reference to the next handler
        return this.interceptor.intercept(req, this.next); }}Copy the code

And use it to wrap our first interceptor like this:

export class AppComponent {
    response: Observable<any>;
    constructor(private backend: HttpXhrBackend) {}

    request() {
        const req = new HttpRequest('GET'.'https://jsonplaceholder.typicode.com/posts/1');
        const handler = new HttpInterceptorHandler(this.backend, new I1());
        this.response = handler.handle(req); }}Copy the code

Now, once we send the request, we can see that custom-header-1 has been added to the request. Here’s an example. With the implementation above, we wrap an interceptor and XHR Backend that references the next handler into HttpInterceptorHandler. Now, this is this is a handler chain.

Let’s add another handler to the chain by wrapping a second interceptor:

export class AppComponent {
    response: Observable<any>;
    constructor(private backend: HttpXhrBackend) {}

    request() {
        const req = new HttpRequest('GET'.'https://jsonplaceholder.typicode.com/posts/1');
        const i1Handler = new HttpInterceptorHandler(this.backend, new I1());
        const i2Handler = new HttpInterceptorHandler(i1Handler, new I2());
        this.response = i2Handler.handle(req); }}Copy the code

You can see the demo here, and everything is fine now, just as we used HttpClient in our original example. What we’ve just done is build a middleware chain of handlers, where each handler executes an interceptor and passes a reference to the next handler to it. Here’s a diagram of the chain:

When we execute the next.handle(Modified) statement in the interceptor, we pass control to the next handler in the chain:

export class I1 implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any> > {const modified = req.clone({setHeaders: {'Custom-Header-1': '1'}});
        // passing control to the handler in the chain
        returnnext.handle(modified); }}Copy the code

Eventually, control is passed to the last Backend handler, which executes requests on the server.

Automatic encapsulation of interceptors

Instead of manually chaining interceptors one by one, we can build the chain of interceptors automatically by injecting all interceptors using the HTTP_INTERCEPTORS token and then using reduceRight to link them together. Let’s do this:

export class AppComponent { response: Observable<any>; constructor( private backend: HttpBackend, @Inject(HTTP_INTERCEPTORS) private interceptors: HttpInterceptor[]) {} request() { const req = new HttpRequest('GET', 'https://jsonplaceholder.typicode.com/posts/1'); const i2Handler = this.interceptors.reduceRight( (next, interceptor) => new HttpInterceptorHandler(next, interceptor), this.backend); this.response = i2Handler.handle(req); }}Copy the code

We need to use reduceRight here to build a chain starting with the last registered interceptor. Using the code above, we get the same chain as the handler chain we built manually. The value returned through reduceRight is a reference to the first handler in the chain.

In fact, the code I wrote above is implemented in Angular using the interceptingHandler function. The exact words were:

Constructs an HttpHandler that applies a bunch of HttpInterceptors to a request before passing it to the given HttpBackend. Meant to be used as a factory function within HttpClientModule.

Construct an HttpHandler that applies a series of HttpInterceptors to requests before passing them to a given HttpBackend. Can be used as a factory function in HttpClientModule.

(incidentally, post the source code below 🙂

export function interceptingHandler(
    backend: HttpBackend, interceptors: HttpInterceptor[] | null = []) :HttpHandler {
  if(! interceptors) {return backend;
  }
  return interceptors.reduceRight(
      (next, interceptor) = > new HttpInterceptorHandler(next, interceptor), backend);
}
Copy the code

Now we know how to construct a chain of handlers. One last thing to note about HTTP handlers is that the interceptingHandler defaults to HttpHandler:

@NgModule({
  providers: [
    {
      provide: HttpHandler,
      useFactory: interceptingHandler,
      deps: [HttpBackend, [@Optional(), @Inject(HTTP_INTERCEPTORS)]],
    }
  ]
})
export class HttpClientModule {}
Copy the code

Therefore, the result of executing this function is that a reference to the first handler in the chain is injected into the HttpClient service and used.

Build an Observable stream that handles the chain

Ok, now we know that we have a bunch of handlers, each of which executes an associated interceptor and calls the next handler in the chain. The value returned by calling this chain is an Observable stream of type HttpEvents. This flow is often (but not always) generated by the last handler, depending on the concrete implementation of Backend. Other handlers usually just return this stream. Here is the last statement of most interceptors:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    ...
    return next.handle(authReq);
}
Copy the code

So we can show logic like this:

But since any interceptor can return an Observable stream of type HttpEvents, you have plenty of customization opportunities. For example, you can implement your own Backend and register it as an interceptor. Or implement a caching mechanism that returns the cache immediately if it is found without passing it on to the next handler:

In addition, since each interceptor has access to the Observable stream returned by the next interceptor (by calling next.handler()), we can modify the returned stream by adding custom logic through the RxJs operator.

Build the HttpClient Observable stream

If you read the previous sections carefully, you may now be wondering whether the HTTP Events flow created by the processing chain is exactly the same as the flow returned by calling HttpClient methods such as GET or POST. Yi… No! The implementation process is more interesting.

HttpClient uses the RxJS create operator of to turn the request object into an Observable stream and returns it when calling HttpClient’s HTTP Request method. The handler chain is processed synchronously as part of the stream, and the Returned Observable is flattened using the concatMap operator. The key implementation is in the Request method, because all API methods like GET, POST, or DELETE simply wrap the request method:

const events$: Observable<HttpEvent<any>> = of(req).pipe(
    concatMap((req: HttpRequest<any>) = > this.handler.handle(req))
);
Copy the code

In the code snippet above, I replaced the old technology call with PIPE. If you’re still confused about how concatMap works, you can read and learn to combine RxJs sequences with super intuitive interactive charts. Interestingly, there’s a reason handler chains execute in the Observable stream that starts with “of.” Here’s an explanation:

Start with an Observable.of() the initial request, and run the handler (which includes all interceptors) inside a concatMap(). This way, the handler runs inside an Observable chain, which causes interceptors to be re-run on every subscription (this also makes retries re-run the handler, including interceptors).

Initial requests are made by observable.of () and handlers (including all interceptors) are run in concatMap(). Thus, the handler runs in a chain of Observables, which causes the interceptor to re-run on each subscription (and re-run the handler, including the interceptor, on retries).

Process the ‘observe’ request option

The initial Observable stream created by HttpClient emits all HTTP events such as HttpProgressEvent, HttpHeaderResponse, or HttpResponse. But we know from the documentation that we can specify the event we are interested in by setting the observe option:

request() {
    const url = 'https://jsonplaceholder.typicode.com/posts/1';
    this.response = this.http.get(url, {observe: 'body'});
}
Copy the code

With {observe: ‘body’}, the Observable stream returned from the GET method emits only the body part of the response. Other options for Observe include Events and Response and response is the default. At the beginning of my exploration of the handler chain implementation, I pointed out that the stream returned by calling the handler chain emits all HTTP events. It is HttpClient’s responsibility to filter events according to observe’s parameters.

This means that the implementation of the HttpClient return stream, as I demonstrated in the previous section, needs a little tweaking. What we need to do is filter these events and map them to different values based on the observe parameter value. The next simple implementation:


const events$: Observable<HttpEvent<any>> = of(req).pipe(...)

if (options.observe === 'events') {
    return events$;
}

const res$: Observable<HttpResponse<any>> =
    events$.pipe(filter((event: HttpEvent<any>) = > event instanceof HttpResponse));

if (options.observe === 'response') {
    return res$;
}

if (options.observe === 'body') {
    return res$.pipe(map((res: HttpResponse<any>) = > res.body));
}
Copy the code

Here, you can find the source code.

Need for immutability

An interesting passage in the document on immutability goes like this:

Interceptors exist to examine and mutate outgoing requests and incoming responses. However, it may be surprising to learn that the HttpRequest and HttpResponse classes are largely immutable. This is for a reason: because the app may retry requests, the interceptor chain may process an individual request multiple times. If requests were mutable, a retried request would be different than the original request. Immutability ensures the interceptors see the same request for each try.

Although interceptors have the ability to alter requests and responses, HttpRequest and HttpResponse instances have readonly attributes, so they are largely immutable objects. There is good reason to make them immutable: the application may retry sending a request many times before it succeeds, which means that the list of interceptors may process the same request multiple times. If the interceptor can modify the original request object, the retry phase starts with the modified request, not the original request. This immutability ensures that the interceptors see the same original request every time they try again.

Let me elaborate. The request object is created when you invoke any HTTP request method of HttpClient. As I explained in the previous section, this request is used to generate an Observable sequence of Events $and is passed in the handler chain when subscribing. However, the Events $stream may be retried, meaning that the original request object created outside the sequence may fire the sequence multiple times. But interceptors should always start with the original request. If the request is mutable and can be modified during an interceptor run, this condition does not apply to the next interceptor run. Since a reference to the same request object will be used multiple times to start an Observable sequence, the request and all its components, such as HttpHeaders and HttpParams, should be immutable.