#Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(map(data => {
// console.log('data', data) response result
return {result: data};
}));
}
}
In #Get, i can see the response result
But in the case of #Post, the data is not visible. Why?
Related
I have a NestJS TS application that has an xml endpoint. I want to validate an xml body. Here's how I went with parsing xml to js object:
#Injectable()
export class XmlToJsInterceptor implements NestInterceptor {
constructor(private parser: CxmlParserService) {}
public intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const req: XmlRequest<unknown> = context.switchToHttp().getRequest();
if (req.rawBody) {
req.xml = this.parser.convertXMLToObject(req.rawBody) as unknown;
}
return next.handle();
}
}
export const XmlBody = createParamDecorator((data: unknown, ctx: ExecutionContext): unknown => {
const request: XmlRequest<unknown> = ctx.switchToHttp().getRequest();
return request.xml;
});
And I use it like this:
#UseInterceptors(XmlToJsInterceptor)
#Controller('/endpoint')
export class MyController {
#Post('/')
#HttpCode(HttpStatus.OK)
async handleEndpoint(
#XmlBody() body: MyClassValidator,
): Promise<void> {
Now I want to use class-validator to check if xml request has a proper structure. I thought to extend XmlBody nestjs param decorator to include validation and manually call class-validator like this:
export const XmlBody = createParamDecorator((data: unknown, ctx: ExecutionContext): unknown => {
const request: XmlRequest<unknown> = ctx.switchToHttp().getRequest();
const validatedConfig = plainToClass(HOWDO_I_GET_PARAM_CLASS, request.xml, {
enableImplicitConversion: true,
});
const errors = validateSync(validatedConfig, { skipMissingProperties: false });
if (errors.length > 0) {
throw new Error(errors.toString());
}
return request.xml;
});
But I don't know to get typescript class from annotated parameter.
I am developing a Nest.js project.
I have a interceptor which simply logs request parameters:
#Injectable()
export class MyRequestInterceptor implements NestInterceptor {
constructor(private logger: MyLogger) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<string> {
const http = context.switchToHttp();
const request = http.getRequest();
const params = request.params;
// output is {}, no parameters showing
this.logger.info(JSON.stringify(params));
return next.handle()
}
}
When I send request GET http://localhost:9001/data?foo=1,2, I expect to see log for foo=1,2 but I see empty object {}. So, where am I wrong? How to access query parameters of request in Nest.js interceptor?
Try request.query
#Injectable()
export class MyRequestInterceptor implements NestInterceptor {
constructor(private logger: MyLogger) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<string> {
const http = context.switchToHttp();
const request = http.getRequest();
const query = request.query;
this.logger.info(JSON.stringify(query));
return next.handle()
}
}
example log interceptor
#Injectable()
export class LogRequestInterceptor implements NestInterceptor {
constructor(
) { }
async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> {
const http = context.switchToHttp();
const request = http.getRequest();
const id = v4();
const { params, query, body, headers, user } = request;
const { url, method } = request.raw;
console.log('___________________________________________________________________________');
console.log('url : ', url);
console.log('method : ', method);
console.log('params : ', params);
console.log('query : ', query);
console.log('body : ', body);
console.log('headers : ', headers);
console.log('id : ', id);
// const message = { id, type: 'request', method, url, params, query, body, headers, user: (user || {}) };
// this.logger.log(message);
return next.handle()
.pipe(tap(data => {
}),
catchError(err => {
const { message: error, status, stack } = err;
const errorMessage = {
id, type: 'error', method, url, params, query, body, headers,
user: (user || {})._id, data: { error, status, stack },
};
console.log('err', err);
return throwError(err);
}));
}
}
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
},
});
});
});
});
});
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;
}))
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[]) {
...
}
}