Search code examples
javascripttypescriptrxjsnestjsinterceptor

How can I create a custom response in a NestJS interceptor in the caughtError operator?


I am probably missing something obvious here. I am using NestJS and want to have an interceptor that catches all errors and create a custom response for this case. I cannot use a global exception filter that catches all errors because I have a second interceptor that logs all requests with their response. The error handler does add some data for the logger, so the order is important here (first the error handler than the logger). Here is the code for the error handler:

@Injectable()
export class UnknownExceptionsResponder implements NestInterceptor {
  constructor(private readonly httpAdapter: HttpServer) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next
        .handle()
        .pipe(
            catchError((error) => {
              const ctx = context.switchToHttp()
              const req = ctx.getRequest()
              req.info = Object.assign({ error }, req.info)

              let status = 500
              const response: any = {
                code: 'unknown_error',
                description: 'Internal server error'
              }

              if (error.exposeCustom_) {
                status = error.status || 500
                response.code = error.message || 'unknown_error'

                if (error.description) {
                  response.description = error.description
                }
                if (error.exposeMeta) {
                  response.meta = error.exposeMeta
                }
              }


              this.httpAdapter.reply(ctx.getResponse(), response, status);
              return EMPTY
            }),
        );
  }
}

I have tried a few different combinations here. The example above does give me the correct response but leads to ERROR [ExceptionsHandler] no elements in sequence. I have also tried this:

return scheduled([{error: error}], asyncScheduler)

This at least calls my logger interceptor which logs the info object, but than ends in the error ERROR [ExceptionsHandler] Cannot set headers after they are sent to the client which I think I understand why. But there is nothing that worked to get the right response when I deleted the reply call - which is probably the solution?

// this.httpAdapter.reply(ctx.getResponse(), response, status);
return scheduled([response], asyncScheduler)

This for example does give me status 200 with the correct response body.

How can I get the desired effect? The custom status and custom body.


Solution

  • Here is how I solved it. I was using the exception interceptor and the logging interceptor in the wrong order.

    Now I used them like this:

    app.useGlobalInterceptors(new UnknownExceptionsResponder(), new LoggingInterceptor(serviceName))
    

    The Logging interceptor will do the following on error:

    catchError((error, caught) => {
      req.info = Object.assign({ error }, req.info)
      log()
    
      return throwError(() => error)
    }),
    

    In the log() function it will log the info object. The exception logger now looks like this:

    @Injectable()
    export class UnknownExceptionsResponder implements NestInterceptor {
    
      intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        return next
            .handle()
            .pipe(
                catchError((error) => {
                  let status = 500
                  const response: any = {
                    code: 'unknown_error',
                    description: 'Internal server error'
                  }
    
                  if (error.exposeCustom_) {
                    status = error.status || 500
                    response.code = error.message || 'unknown_error'
    
                    if (error.description) {
                      response.description = error.description
                    }
                    if (error.exposeMeta) {
                      response.meta = error.exposeMeta
                    }
                  }
    
                  return throwError(() => new HttpException(response, status, {cause: error}))
                }),
            );
      }
    }
    

    With that I basically translate my backend internal error type to the NestJS error type. This will lead to the correct response and code to be send to the client. The only problem that remains for me is that a cors error is not logged. Don't know why. It seems that the cors policy will block it request from even reaching the interceptors.