Search code examples
node.jstypescriptexpressrxjsnestjs

NestJS : Interceptor both map and catchError


I need a NestJS interceptor that archives requests, both in exceptional and happy-path cases. Created as follows:

public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {

    if (!this.reflector.get<boolean>(RequestMetaData.IS_PUBLIC_ROUTE, context.getHandler())) {
        return next.handle().pipe(
          map(data => {
              const host = context.switchToHttp();
              const req = host.getRequest();
              const resp = host.getResponse();
              this.persistRequest(data, req, resp)
                .then(() => this.logger.log(`Request logged`))
                .catch(e => {
                    this.logger.error(`Error logging request: ${e.message}`);
                });
              return data;
          }));
    }
    return next.handle();
}

Problem:

This only logs the happy path. Because I'm not familiar with RxJS I created another to persist errors. Eg:

public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next
      .handle()
      .pipe(
        catchError(err => {
            return throwError(err);
        })
      );
}

How can I define a single interceptor that archives both paths?


Solution

  • I think you’re using the wrong operator here. The inner map returns a publisher as well.

    You should transform the outer publisher with a flatMap so that you create one flow, not a nested flow.

    Here is an example in pure RxJS 6:

    import { of, EMPTY } from 'rxjs';
    import { map, flatMap, catchError } from 'rxjs/operators';
    
    of(3,2,1,0,1,2,3).pipe(
      flatMap(v => {
        return of(v).pipe(
          map(x => {    
            if(x===0) throw Error();
            return 6 / x;
          }), 
          catchError(error => {
            console.log("Shit happens")
            return EMPTY
          }
        )
        )
      } 
    ))
    .subscribe(val => console.log("Request " + val + " logged "));
    

    Each request (here numbers) is flat map into a thing that persists stuff. Flat map means, that the persister returns an observable again. See https://rxjs-dev.firebaseapp.com/api/operators/flatMap

    The error handling of those inner observables is done via the catchError operator. It logs the error and than returns an empty observable to indicate that the inner observable is "dead". You could return another observable here and than the inner would continue.

    The outer observable, that is your incoming requests, continue each along the way.

    I have create a stackblitz app here:

    https://rxjs-qkqkm2.stackblitz.io

    Good luck with the NestJS and all the different versions of RxJS. This above is version 6.

    Edit:

    RxJS tap method is a good approach for handling side effects. Implement the intercept method as follows:

    public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    
        if (!this.reflector.get<boolean>(RequestMetaData.IS_PUBLIC_ROUTE, context.getHandler())) {
    
            const host = context.switchToHttp();
            const req = host.getRequest();
            const resp = host.getResponse();
    
            return next.handle().pipe(
              tap({
                  next: (val) => {
                      this.persistRequest(val, req, resp);
                  },
                  error: (error) => {
                      this.persistRequest(AppError.from(error), req, resp);
                  }
              })
            );
        }
        return next.handle();
    }