How to instrument a controller in nestjs interceptor? - node.js

I want to instrument every method of a nestjs controller for APM purposes.
I wrote the following interceptor in order to instrument the controller invocation.
However, I do not know how to properly wrap the call to next.handle().
I do not have any experience using RxJS Observables.
Question: Is it possible to wrap the invocation properly and if so how?
The current approach seems to measure the controller's execution time but does not set a correct tracer scope for the controller's method. I guess the issue is that next.handle() must be wrapped too.
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "#nestjs/common";
import { Reflector } from "#nestjs/core";
import { Observable } from "rxjs";
import { PATH_METADATA } from '#nestjs/common/constants';
import tracer from "dd-trace";
#Injectable()
export class ApmInterceptor implements NestInterceptor {
constructor(private readonly reflector: Reflector) {}
public intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const request: Request = context.switchToHttp().getRequest();
const path = this.reflector.get<string[]>(PATH_METADATA, context.getHandler());
const method = request.method;
const observable = next.handle();
tracer.trace(`[${method}] ${path}`, () => new Promise((resolve, reject) => {
observable.subscribe({
complete: resolve,
});
}));
return observable;
}
}

Faced a similar issue using OpenTelemetry-js, in order to set the correct scope I've to wrap the handle() Observable into an Async promise to set the context, and then wrap the promise again as Observable for the rxjs pipeline (Observable -> Promise -> Observable)
import {from, Observable} from 'rxjs';
...
async intercept(executionContext: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request: Request = context.switchToHttp().getRequest();
const path = this.reflector.get<string[]>(PATH_METADATA, context.getHandler());
const method = request.method;
const observable = tracer.trace(`[${method}] ${path}`, () => new Promise((resolve, reject) => {
return next.handle().toPromise();
}));
return observable.pipe(
map(value => {
// Here you can stop your trace manually
return value;
}),
catchError(error => {
// Here you can stop your trace manually
throw error;
}))
}
For OpenTelemetry you have to create/stop the span and set the correct context:
const span = trace.getTracer('default').startSpan(spanName);
const observable = from(context.with(trace.setSpan(context.active(), span), async () => {
return next.handle().toPromise();
}));
return observable.pipe(
map(value => {
span.stop();
return value;
}),
catchError(error => {
span.addEvent('error', {error: error});
span.stop();
throw error;
}))

Related

I created custom nest js guard which is running before every request and I have added a property called all() on the request in that guard

I have created a interface which is extending the request (of express)
I m adding property called all() in it
which contains the body , query and params in it
import { Request as BaseRequest } from 'express';
export interface Request extends BaseRequest {
all(): Record<string, any>;
}
this is the interface
which is extending the express request
and i m adding this all() property using the guard
this is the implementation of it
#Injectable()
export class RequestGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
this.requestHelper(context.switchToHttp().getRequest());
return true;
}
requestHelper(request: any): any {
const all = function (): Record<string, any> {
return {
...request.query,
...request.body,
...request.params,
};
};
request.all = all;
return request;
}
}
in the main.ts file i have used this guard
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '#nestjs/common';
import { RequestGuard } from './core/guards/request.guard';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
}),
);
app.useGlobalGuards(new RequestGuard());
await app.listen(3000);
}
bootstrap();
and i have tried consoling the all() property in the guard and it's working
its mean request is flowing in it
when i try to get this all() property in my controller then it showing
Cannot read properties of undefined (reading 'all')
That's how i m calling it
import {
Controller,
Get,
Post,
Param,
Body,
Req,
Res,
UseGuards,
} from '#nestjs/common';
import { RequestGuard } from 'src/core/guards/request.guard';
import { Request } from 'src/core/http/Request';
import { Response } from 'src/core/http/Response';
#UseGuards(RequestGuard)
#Controller('customers')
export class CustomersController {
constructor(private customersService: CustomersService) {}
#Get('/order-data/:id')
async OrderData(#Param('id') id: string, req: Request, #Res() res: Response) {
console.log(req.all());
const data = await this.customersService.allOrdersData(parseInt(id));
return data;
}
}
I m calling the route localhost:3000/customers/order-data/1
console.log(req.all());
It should print {id:{'1'}}
But it's giving error
Cannot read properties of undefined (reading 'all')
You're missing the #Req() for your req property in the OrderData method.

Where is the beest place to put prisma middleware on NestJs?

My prisma.service.ts looks like this:
#Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}
According to Prisma docs I am supposed to put them outside the context of the request handler. That would be the app that I create on main.ts. Putting a middleware there before the app itself is defined looks wierd to me and doesn't work. I'd prefer to put it on the prisma.service.ts file itself
Not sure whether this is the "best" place to register them, but we do it in the constructor of the service along with the logging configuration and it works:
import { INestApplication, Injectable, Logger, OnModuleInit } from "#nestjs/common";
import { Prisma, PrismaClient } from "#prisma/client";
import { ConcurrencyErrorMiddleware } from "./concurrency-error.middleware";
#Injectable()
export class PrismaService extends PrismaClient<Prisma.PrismaClientOptions, "query"> implements OnModuleInit {
private readonly logger = new Logger(PrismaService.name);
constructor() {
super({ log: [{ emit: "event", level: "query" }] });
this.logger.log(`Prisma v${Prisma.prismaVersion.client}`);
this.$on("query", (e) => this.logger.debug(`${e.query} ${e.params}`));
this.$use(ConcurrencyErrorMiddleware());
}
async onModuleInit(): Promise<void> {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication): Promise<void> {
this.$on("beforeExit", async () => {
await app.close();
});
}
}
// An example of such a middleware.
import { Prisma } from "#prisma/client";
export function ConcurrencyErrorMiddleware<T extends Prisma.BatchPayload = Prisma.BatchPayload>(): Prisma.Middleware {
return async (params: Prisma.MiddlewareParams, next: (params: Prisma.MiddlewareParams) => Promise<T>): Promise<T> => {
const result = await next(params);
if (
(params.action === "updateMany" || params.action === "deleteMany") &&
params.args.where.version &&
result.count === 0
) {
throw new ConcurrencyError();
}
return result;
};
}

How to Test Timeout Interceptor in Nestjs Using Jest

I can't find any explanation on how to test interceptors in NestJS.
Please help me to test the Interceptor using jest?
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from "#nestjs/common";
import { Observable, throwError, TimeoutError } from "rxjs";
import { catchError, timeout } from "rxjs/operators";
#Injectable()
export class TimeoutInterceptor implements NestInterceptor {
constructor(private readonly interval: number) {}
intercept(_context: ExecutionContext, next: CallHandler): Observable<any> {
if (this.interval > 0) {
return next.handle().pipe(
timeout(this.interval),
catchError((error) => {
if (error instanceof TimeoutError) {
return throwError(new RequestTimeoutException(`The operation timed out. `));
}
return throwError(error);
}),
);
}
return next.handle();
}
}
I tried to write unit tests for this interceptor once but I didn't like it :/ Look: https://gist.github.com/micalevisk/33d793202541f044d8f5bccb81049b94
I tried to find similar issues out there, hoping that anyone would be willing to answer but I still got nothing until today. Nevertheless, I try to figure it out all alone. For someone out there who's probably looking for a possible workaround.
Let's take a look at the simple timeout interceptor example from nestjs docs
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, RequestTimeoutException } from '#nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
#Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(() => new RequestTimeoutException());
}
return throwError(() => err);
}),
);
};
};
The possible unit test to test the error inside the observable is to mock the return value after a specified time (delay). In this case, it would be more than 5000. This can be achieved by utilizing the delay rxjs operator.
So the appropriate unit test for this timeout interceptor would be something similar to this.
describe('TimeoutInterceptor', () => {
const executionContext = mockedExecutionContext; // your mocked execution context
const callHandler = mockedCallHandler; // your mocked call handler
let timeoutInterceptor: TimeoutInterceptor;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [TimeoutInterceptor],
}).compile();
timeoutInterceptor = moduleRef.get<TimeoutInterceptor>(TimeoutInterceptor);
});
describe('when intercept is called', () => {
const returnedValue: [] = [];
describe('and the request time is not exceeded the max allowed timeout in ms', () => {
it(`should return the to be returned value`, (done: any) => {
callHandler.handle.mockReturnValue(
of(returnedValue).pipe(
delay(2500), // below 5000
),
);
timeoutInterceptor.intercept(executionContext, callHandler).subscribe({
next(value) {
expect(value).toStrictEqual(returnedValue);
},
complete() {
done();
},
});
});
});
describe('and the request time exceeded (+1000ms) the max allowed timeout in ms', () => {
it(`should throw ${RequestTimeoutException.name}`, (done: any) => {
callHandler.handle.mockReturnValue(
of(returnedValue).pipe(delay(5000 + 1000)), // 5000 + 1000ms
);
timeoutInterceptor.intercept(executionContext, callHandler).subscribe({
error(err) {
expect(err).toBeInstanceOf(RequestTimeoutException);
done(); // don't forget to invoke this done, or the test will be hang
},
});
});
});
});
});

NestJs request and response interceptor unit testing

I would like to log the incoming requests and outgoing responses for my API. I created a request interceptor and a response interceptor as described here
https://docs.nestjs.com/interceptors
So the request interceptor only logs the request object
#Injectable()
export class RequestInterceptor implements NestInterceptor {
private readonly logger: Logger = new Logger(RequestInterceptor.name, true);
public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const { originalUrl, method, params, query, body } = context.switchToHttp().getRequest();
this.logger.debug({ originalUrl, method, params, query, body }, this.intercept.name);
return next.handle();
}
}
and the response interceptor waits for the outgoing response and logs the status code and response object later on
#Injectable()
export class ResponseInterceptor implements NestInterceptor {
private readonly logger: Logger = new Logger(ResponseInterceptor.name, true);
public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const { statusCode } = context.switchToHttp().getResponse();
return next.handle().pipe(
tap((responseData: any) =>
this.logger.debug({ statusCode, responseData }, this.intercept.name),
),
);
}
}
I would like to test them but unfortunately have almost no experience in testing. I tried to start with the request interceptor and came up with this
const executionContext: any = {
switchToHttp: jest.fn().mockReturnThis(),
getRequest: jest.fn().mockReturnThis(),
};
const nextCallHander: CallHandler<any> = {
handle: jest.fn(),
};
describe('RequestInterceptor', () => {
let interceptor: RequestInterceptor;
beforeEach(() => {
interceptor = new RequestInterceptor();
});
describe('intercept', () => {
it('should fetch the request object', (done: any) => {
const requestInterception: Observable<any> = interceptor.intercept(executionContext, nextCallHander);
requestInterception.subscribe({
next: value => {
// ... ??? ...
},
error: error => {
throw error;
},
complete: () => {
done();
},
});
});
});
});
I currently don't know what to pass into the next callback but when I try to run the test as it is it says that the requestInterception variable is undefined. So the test fails before reaching the next callback. So the error message I get is
TypeError: Cannot read property 'subscribe' of undefined
I also tried to test the response interceptor and came up with this
const executionContext: any = {
switchToHttp: jest.fn().mockReturnThis(),
getResponse: jest.fn().mockReturnThis()
};
const nextCallHander: CallHandler<any> = {
handle: jest.fn()
};
describe("ResponseInterceptor", () => {
let interceptor: ResponseInterceptor;
beforeEach(() => {
interceptor = new ResponseInterceptor();
});
describe("intercept", () => {
it("should fetch the statuscode and response data", (done: any) => {
const responseInterception: Observable<any> = interceptor.intercept(
executionContext,
nextCallHander
);
responseInterception.subscribe({
next: value => {
// ...
},
error: error => {
throw error;
},
complete: () => {
done();
}
});
});
});
});
This time I get an error at the interceptor
TypeError: Cannot read property 'pipe' of undefined
Would some mind helping me to test those two interceptors properly?
Thanks in advance
Testing interceptors can be one of the most challenging parts of testing a NestJS application because of the ExecutionContext and returning the correct value from next.
Let's start with the ExecutionContext:
You've got an all right set up with your current context, the important thing is that you have a switchToHttp() method if you are using HTTP (like you are) and that whatever is returned by switchToHttp() has a getResponse() or getRequest() method (or both if both are used). From there, the getRequest() or getResponse() methods should return values that are used from the req and res, such as res.statusCode or req.originalUrl. I like having incoming and outgoing on the same interceptor, so often my context objects will look something like this:
const context = {
switchToHttp: jest.fn(() => ({
getRequest: () => ({
originalUrl: '/',
method: 'GET',
params: undefined,
query: undefined,
body: undefined,
}),
getResponse: () => ({
statusCode: 200,
}),
})),
// method I needed recently so I figured I'd add it in
getType: jest.fn(() => 'http')
}
This just keeps the context light and easy to work with. Of course you can always replace the values with more complex ones as you need for logging purposes.
Now for the fun part, the CallHandler object. The CallHandler has a handle() function that returns an observable. At the very least, this means that your next object needs to look something like this:
const next = {
handle: () => of()
}
But that's pretty basic and doesn't help much with logging responses or working with response mapping. To make the handler function more robust we can always do something like
const next = {
handle: jest.fn(() => of(myDataObject)),
}
Now if needed you can override the function via Jest, but in general this is enough. Now your next.handle() will return an Observable and will be pipable via RxJS operators.
Now for testing the Observable, you're just about right with the subscribe you're working with, which is great! One of the tests can look like this:
describe('ResponseInterceptor', () => {
let interceptor: ResponseInterceptor;
let loggerSpy = jest.spyOn(Logger.prototype, 'debug');
beforeEach(() => {
interceptor = new ResponseInterceptor();
});
afterEach(() => {
loggerSpy.resetMock();
});
describe('intercept', () => {
it('should fetch the request object', (done: any) => {
const responseInterceptor: Observable<any> = interceptor.intercept(executionContext, nextCallHander);
responseInterceptor.subscribe({
next: value => {
// expect the logger to have two parameters, the data, and the intercept function name
expect(loggerSpy).toBeCalledWith({statusCode: 200, responseData: value}, 'intercept');
},
error: error => {
throw error;
},
complete: () => {
// only logging one request
expect(loggerSpy).toBeCalledTimes(1);
done();
},
});
});
});
});
Where executionContext and callHandler are from the values we set up above.
A similar idea could be done with the RequestInterceptor, but only logging in the complete portion of the observer (the subscribe callback) as there are no data points returned inherently (though it would still work either way due to how observables work).
If you would like to see a real-world example (albeit one with a mock creation library), you can check out my code for a logging package I'm working on.

NestJS: Receive form-data in Guards?

I'm looking to see form-data in my NestJS Guards. I've followed the tutorial, however, I'm not seeing the request body for my form-data input. I do see the body once I access a route within my controller, however.
Here's some code snippets of what I'm working with:
module.ts
...
#Module({
imports: [
MulterModule.register({
limits: { fileSize: MULTER_UPLOAD_FILESIZE_BYTES },
}),
],
controllers: [MainController],
providers: [
MainService,
AuthGuard,
],
})
...
AuthGuard.ts
import { Injectable, CanActivate, ExecutionContext } from '#nestjs/common';
import { Observable } from 'rxjs';
#Injectable()
export class AuthGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest(); // body is empty if form-data is used
return true;
}
}
MainController.ts
...
#Post("/upload")
#UseInterceptors(AnyFilesInterceptor())
#UseGuards(AuthGuard)
async upload(
#Body() body: UploadDTO,
#UploadedFiles() files: any[]
): Promise<any> {
console.log(body) // works as expected, whether form-data is used or not
...
}
...
Any feedback would be greatly appreciated!
NestJS guards are always executed before any middleware. You can use multer manually on the request object you get from the context.
import * as multer from 'multer'
...
async canActivate(context: ExecutionContext): Promise<boolean> {
const request: Request = context.switchToHttp().getRequest();
const postMulterRequest = await new Promise((resolve, reject) => {
multer().any()(request, {}, function(err) {
if (err) reject(err);
resolve(request);
});
});
// postMulterRequest has a completed body
return true;
}
If you want to use the #UploadedFiles decorator, you need to clone the request object before modifying it in your guard.
Of course you need to have installed the multer module with:
npm install multer
Posting my solution in-case it helps other devs dealing with the same issue.
To start, I created a middleware to handle the conversion of the multipart form data request. You could also inline this in to your guard if you only have one or two. Much of this code is plagiarised from the source code, and is not fully tested:
const multerExceptions = {
LIMIT_PART_COUNT: 'Too many parts',
LIMIT_FILE_SIZE: 'File too large',
LIMIT_FILE_COUNT: 'Too many files',
LIMIT_FIELD_KEY: 'Field name too long',
LIMIT_FIELD_VALUE: 'Field value too long',
LIMIT_FIELD_COUNT: 'Too many fields',
LIMIT_UNEXPECTED_FILE: 'Unexpected field',
}
function transformException(error: Error | undefined) {
if (!error || error instanceof HttpException) {
return error
}
switch (error.message) {
case multerExceptions.LIMIT_FILE_SIZE:
return new PayloadTooLargeException(error.message)
case multerExceptions.LIMIT_FILE_COUNT:
case multerExceptions.LIMIT_FIELD_KEY:
case multerExceptions.LIMIT_FIELD_VALUE:
case multerExceptions.LIMIT_FIELD_COUNT:
case multerExceptions.LIMIT_UNEXPECTED_FILE:
case multerExceptions.LIMIT_PART_COUNT:
return new BadRequestException(error.message)
}
return error
}
#Injectable()
export class MultipartMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
// Read multipart form data request
// Multer modifies the request object
await new Promise<void>((resolve, reject) => {
multer().any()(req, res, (err: any) => {
if (err) {
const error = transformException(err)
return reject(error)
}
resolve()
})
})
next()
}
}
Then, I applied the middleware conditionally to any routes which accept multipart form data:
#Module({
controllers: [ExampleController],
imports: [...],
providers: [ExampleService],
})
export class ExampleModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(MultipartMiddleware).forRoutes({
path: 'example/upload',
method: RequestMethod.POST,
})
}
}
Finally, to get the uploaded files, you can reference req.files:
#Controller('example')
export class ExampleController {
#Post('upload')
upload(#Req() req: Request) {
const files = req.files;
}
}
I expanded this in my own codebase with some additional supporting decorators:
export const UploadedAttachment = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
return request.files?.[0]
}
)
export const UploadedAttachments = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
return request.files
}
)
Which ends up looking like:
#Controller('example')
export class ExampleController {
#Post('upload')
upload(#UploadedAttachments() files: Express.Multer.File[]) {
...
}
}

Resources