How to handle logging of HttpClient requests?

Royi Namir picture Royi Namir · Feb 20, 2018 · Viewed 8.6k times · Source

I'm using Angular and rxjs 5.x.x . For each http request I must log the response in a centralized place -either if success or fail.

The problem which i'm facing is in a cituation where a catch clause is being called.

For example :

  callRemote() {
    return this.http.get("http://aaa.dom")
                    .catch(f=>Rx.Observable.of(f))
                    .do(f => console.log("logging response both bad and ok..."))
  }

This is an http request which fails. But I must invoke the do operator becuase I need to log it. However if a .catch is called , the .do won't be called. That's why I did this :

  .catch(f=>Rx.Observable.of(f))

So I'm basically catching the error and wrap it as a new observable and continue to the .do function.

So far so good.

The problem now is that ,eventually , when subscribing , I want to know if request failed or OK.

So basically this is how I subscribe :

 this._http.callRemote().subscribe(
      (x => console.log('OK')),
      (x => console.log('ERROR')),
      (() => console.log('COMPLETE'))
    );

Problem is that everything is redirected to the sucess callback function. Even failures. I do understand why it happend - it's becuase I wrapped catch fails as new Observables.

Question

Is it possible that wrapped errors will go to the error callback , and not to the success callback ?

Online demo

Answer

Jota.Toledo picture Jota.Toledo · Feb 20, 2018

As already mentioned in the comments, the catch shouldn't swallow the error in the first place, thats a nogo for sure. By doing that you are simply blocking the error signal in the rxjs chain.

Eventually you could do your logging in the catch and then re throw, but thats kinda overkill for only log side effect.

The simplest option would be to use the error callback arguent in the do operator.

According to this tutorial page, the do operator can take 3 arguments (callback fns), what basically matches the subscribe signature:

1s argument: callback on next

2nd argument: callback on error

3rd argument: callback on complete

So you could refactor into the following:

callRemote() {
    return this.http.get("http://aaa.dom")
                    .do(
                      response => console.log("logging response both bad and ok..."), 
                      error => console.log("Something exploded, call 911");
  }

So basically you could attach that operation to every single HttpClient call in your base code. Pretty neat, no?

Wait wait! This might look neat on a first glance, but it will backfire at the very same moment that you want to:

  • Modify the logging behavior
  • Refactor in any way

Why?

You are going to basically monkey-patch every single possible back end call with that do() operation. And if making 1 change to that logic means changing code in more than 3 places, there something smelly going on.

A better approach

With the introduction of the HttpClient, another API was added: the HttpInterceptor API.

Basically, you could intercept all your outgoing requests in a single point.

How? As it follows:

First step, create an injectable service that you can use to capsulate the logging logic;

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';
import { AuthService } from './auth/auth.service';
import { Observable } from 'rxjs/Observable';
import { tap } from 'rxjs/operators'; // fancy pipe-able operators

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
       tap(
           response => console.log("Oh boy we got an answer"), 
           error => console.log("Something might be burning back there")
       ));
  }
}

Second step, make angular aware of the existence of the LoggingInterceptor by providing it through a token:

import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule, ClassProvider } from '@angular/core';

import { LoggingInterceptor } from '../some-place';

const LOGGING_INTERCEPTOR_PROVIDER: ClassProvider = {
   provide: HTTP_INTERCEPTORS ,
   useClass: LoggingInterceptor,
   multi: true
};

@NgModule({
   ...
   providers: [
      LOGGING_INTERCEPTOR_PROVIDER
   ]
   ...
})
export class AppModule {}

And thats it! Now you can log all your outgoing requests and do some other cool stuff with those logs if necessary, in a truly centralized way.