Neeraj
Neeraj

Reputation: 483

NestJS: Execute Interceptor before Exception Filter

I need to do event tracking for certain API calls(based on handler name) and it is basically a function that records the activity.

At the moment I have a interceptor that does this event tracking and it works fine.

But problem is whenever there is an error, i am catching it using a global exception filter AND this returns the response immediately without entering the interceptor and my event tracking code(I need the class name, handler name and returned data if available).

Here is my setup

app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { APP_INTERCEPTOR, APP_FILTER } from '@nestjs/core';

import { AuthModule } from './auth/auth.module';
import { MembersModule } from './members/members.module';

import { DatabaseConfigService } from './_database/database-config.service';
import { EventTrackerService } from './event-tracker/event-tracker.service';

import { LoggingInterceptor } from './_interceptors/logging.interceptor';
import { EventsTrackerInterceptor } from './_interceptors/events-tracker.interceptor';

import { AllExceptionsFilter } from './_filters/all-exception.filter';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    MongooseModule.forRootAsync({
      useClass: DatabaseConfigService,
    }),
    AuthModule,
    MembersModule,
  ],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
    {
      provide: APP_INTERCEPTOR,
      useClass: EventsTrackerInterceptor,
    },
    {
      provide: APP_FILTER,
      useClass: AllExceptionsFilter,
    },
    EventTrackerService,
  ],
})
export class AppModule {}

events-tracker.interceptor.ts

import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { EventTrackerService } from '../event-tracker/event-tracker.service';

@Injectable()
export class EventsTrackerInterceptor implements NestInterceptor {
  constructor(private readonly eventTrackerService: EventTrackerService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const response = context.switchToHttp().getResponse();
    const className = context.getClass().name;
    const handler = context.getHandler().name;

    return next.handle().pipe(
      tap(data => {
        this.eventTrackerService.handleEvent(
          request.url,
          className,
          handler,
          data,
        );
      }),
    );
  }
}

all-exception.filter.ts

import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { EventTrackerService } from '../event-tracker/event-tracker.service';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  constructor(private readonly eventTrackerService: EventTrackerService) {}
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.FORBIDDEN;

    const message =
      typeof exception.message === 'string'
        ? exception.message
        : exception.message.message;

    const errorResponse = {
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: message || 'Something went wrong',
    };
    Logger.error(
      `${request.method} ${request.url} ${status}`,
      JSON.stringify(errorResponse),
      'AllExceptionsFilter',
      true,
    );
    response.status(status).json(errorResponse);
  }
}

My ultimate question is: How do I call the events tracker in interceptor despite the exception being throw?

Upvotes: 2

Views: 4469

Answers (1)

Jay McDoniel
Jay McDoniel

Reputation: 70131

You have one of two options with the interceptor: 1) implement a catchError branch of the pipe, do your logic and re-throw the error, or 2) tap can take in an object similar to subscribe for next, error, complete and run certain functions for what happens with the observable. The tap could look like

return next.handle().pipe(
  tap({
    next: (data) => this.eventTrackerService.handleEvent(
          request.url,
          className,
          handler,
          data,
        ),
    error: (err) => this.eventTrackerService.handleEvent(
          request.url,
          className,
          handler,
          error,
        )
  })
);

And now you have a tap that works on success and error, but now you're also passing the error instead of what data was to be returning to your tracker.

Upvotes: 5

Related Questions