Interceptor not catching error thrown by guard in nestjs - nestjs

I have a global guard that is registered in the application in common.module.ts which is a global module.
const HeaderGuardGlobal = {
provide: APP_GUARD,
useClass: HeaderGuard
};
#Global()
#Module({
imports: [ LoggerModule ],
providers: [ HeaderGuardGlobal ],
exports: []
})
header.guard.ts:
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const userName = request.headers[HEADERS.USER_NAME];
if(!userName) {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
I have a control-scoped interceptor authenticate-header.interceptor.ts
#Injectable()
export class setAuthenticateHeaderInterceptor<T> implements NestInterceptor<T, Response<T>> {
public constructor() {}
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
const req = context.switchToHttp().getRequest();
const res = context.switchToHttp().getResponse();
return next
.handle()
.pipe(
catchError(err => {
console.log('ERROR: ', err);
res.setHeader('sampleKey', 'sampleValue');
return throwError(err);
})
)
}
}
user.controller.ts:
#Controller('user')
#UseInterceptors(setAuthenticateHeaderInterceptor)
export class ClientController {
What I'm trying to achieve is that when header.guard.ts throws Forbidden exception, the authenticate-header.interceptor.ts would catch the exception and will propagate it to the global http-filter, but before doing that I want to add a header to the response object.
The issue I'm facing is when the exception is being thrown by the guard, the interceptor is unable to catch it. However, when the same error is thrown either from the route handler or from service, the interceptor is able to catch it.
I went through the request-lifecycle to understand the execution context, and found the below statement in interceptors section.
any errors thrown by pipes, controllers, or services can be read in the catchError operator of an interceptor.
The statement doesn't say anything about guards, so I'm assuming what I'm trying to achieve is not possible.
I'm unable to figure out why an error thrown inside a guard is not being caught by the interceptor. The above code snippets only include the parts which I thought were necessary for the question, if anybody feels like more info is needed. then I'll provide it.

The docs correctly state, as you mentioned,
any errors thrown by pipes, controllers, or services can be read in the catchError operator of an interceptor.
Guards, as noted below in the summary section are executed before interceptors are, and as such, their errors are not able to be caught in the interceptor's catchError method. Your best bet would be to make a filter that extends your GlobalFilter, add in your logic there, then call super.catch(exception) to call the rest of the logic.

Related

How to catch validation error message - Nestjs Gateway Socket.io

I have a validation pipe that checks if a user-sent JSON data is valid. Validator is working great, but I can not catch the error and then send it to the client. I know about exeptionFactory property in ValidationPipe constructor, but I still can not catch an error and it still logs in the console.
[Nest] 11820 - 01/07/2023, 11:12:25 PM ERROR [WsExceptionsHandler] Bad Request Exception
BadRequestException: Bad Request Exception
Here is a code
#SubscribeMessage(Stream.Transactions)
#UseGuards(JwtAuthGuard)
#UsePipes(new ValidationPipe())
handleTransactions(
clien: any,
#MessageBody() data: TransactionObject,
) {
let req = this.streamService.transaction(data)
return { event: Stream.Transactions, data: req }
}
I think you can create a filter to get the error and return some specific data. But you can do it in a couple ways: Creating a Websocket exception filter to catch the error or use the exceptionFactory you mention in your question to generate a WsException and catch into the filter.
The main problem (if I'm not wrong) is the pipe does not return a WsException but a BadRequestException.
So to use an exception filter, how the exception is Bad Request, you can use this one:
#Catch(BadRequestException)
export class BadRequestExceptionsFilter extends BaseWsExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
// Here you have the exception and you can check the data
const wsException = new WsException(exception.getResponse())
super.catch(wsException, host);
}
}
Note how this code follows the documentation
And now not only you can get and read the exception but also you can create a properly WsException.
To use this you can add #UseFilters(BadRequestExceptionsFilter) into your gateway.
Another way is to catch WS and HTTP exceptions and handle properly you want, something similar to this example. The idea can be to catch the HTTP exception only to get Bad Request so your desired context will be always WS:
#Catch(WsException, HttpException)
export class WsAndHttpExceptionFilter {
public catch(exception: HttpException, host: ArgumentsHost) {
// Here you have the exception and you can check the data
const ctx = host.switchToWs()
const client = ctx.getClient() as WebSocket;
client.send(JSON.stringify({ /* ... */ }))
}
}
Or also you can try to create the exceptionFactory to return the WsException.
Into the decorator:
#UsePipes(new ValidationPipe({
exceptionFactory(validationErrors: ValidationError[] = []) {
// Here are the errors
if (this.isDetailedOutputDisabled) {
return new WsException();
}
const errors = this.flattenValidationErrors(validationErrors);
return new WsException(errors);
}
}))
Check how factories are done into the project and how this code tries to follow the same way but returning WsException.
Or in a class extending ValidationPipe:
#Injectable()
export class WSValidationPipe extends ValidationPipe {
createExceptionFactory() {
return (validationErrors: ValidationError[] = []) {
// Here are the errors
if (this.isDetailedOutputDisabled) {
return new WsException();
}
const errors = this.flattenValidationErrors(validationErrors);
return new WsException(errors);
}
}
}
By the way, you can also #Catch(WsException) (and only this exception which is clearer) once are thrown if it is util for you to return the data you want:
#Catch(WsException)
export class WebsocketExceptionsFilter extends BaseWsExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToWs()
const client = ctx.getClient() as WebSocket;
const data = ctx.getData();
client.send(JSON.stringify({
event: 'error',
ok: false,
error: exception.getError(),
data: data // Or whatever you want to add
}))
}
}
I can't test now all this code but I've used some of these snippets and works for me, hope it helps.

Multiple routes with same path - use next route (e.g. feature toggle, a/b testing)

does anybody know if there is an option to implement multiple routes in nestjs with the same path.
Lets say:
/checkout in BasketControllerNew
/checkout in BasketController
At the first controller, there is a guard which decides if the user can access the /checkout at BasketControllerNew.
Returning false will throw an ForbiddenException, fine.
If I throw a custom exception, the second route will also not be executed.
Filters seems also not able to jump to the next route.
I want to implement feature toggle functionality.
In this case for example a new checkout process with a completely new controller file.
The new checkout process should be tested by some users for example or can be enabled and disabled by feature toggles.
My preferred behaviour would be to throw a custom exception like DisabledRouteException or something like that which means "please try the next matching route".
Another possible usecase could be:
A cms with a wildcard route which tries to find pages because they do not have a specific prefix at their path
There will be a module which uses routes prefixed with moduleA/.
As long as the module is not enabled for a specific customer, the moduleA/* should be handled by the wildcard route because there can be pages at this path.
Has anybody an idea how to implement such things?
Thanks a lot for your support and keep healthy.
Daxi
I have found a solution, which works for me:
Summary:
Multiple routes with same path
Guard which throws a Exception
Filter which catches the exception and executes the next route.
Example code for controller:
#Controller()
export class TestController {
#Get()
#UseGuards(TestGuard)
getHello(): string {
return "Hello";
}
#Get()
#UseGuards(TestGuard)
getHello2(): string {
return "Hello2";
}
#Get()
getHello3(): string {
return "Hello3";
}
}
Example code for guard:
#Injectable()
export class TestGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
// add logic here to switch between execution or skip
throw new NotFoundException('Module not active');
}
}
Example code for filter:
#Catch(NotFoundException)
export class TestFilter implements ExceptionFilter {
catch(exception: NotFoundException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const next = ctx.getNext();
next();
}
}
Enable the filter globaly in main.ts:
async function bootstrap() {
const app = await NestFactory.create(TestModule);
app.useGlobalFilters(new TestFilter());
await app.listen(3000);
}
To get a proper handling, the code can be improved by generating a custom exception and filter only that, currently all 404s will trigger the next route.
At this example the guard will always throw the exception which ends up at this behaviour:
guard for getHello will throw the NotFoundException
filter will trigger the next route
guard for getHello2 will throw also the NotFoundException
filter will again trigger the next route
getHello3 will be executed (no guard active)
My code is only a small POC which can be improved as described and should be improved.
The test was only a very small quick and dirty solution, but if you will need a similar solution, now you can start at the same point.
Kind regards,
Daxi

Nestjs: Retrieve the request / context from a Decorator

I am working on a NestJS project,
I'm trying to get the executionContext accessible in a logger to filter the logs by request.
I have one logger instance per injectable, and I would like to keep this behavior (So the scope of the injectable is default).
To do this, I'm trying to create a decorator that gets the context from the request and passes it to the child services (as in the logger), to finally get the context in the logger...
I'm not sure to be clear... For now, here is my code:
export const Loggable = () => (constructor: Function) => {
for (const propertyName of Reflect.ownKeys(constructor.prototype)) {
let descriptor = Reflect.getOwnPropertyDescriptor(constructor.prototype, propertyName);
const isMethod = descriptor.value instanceof Function;
if (!isMethod)
continue;
const originalMethod = descriptor.value;
const routeArgsMetada = Reflect.getMetadata(ROUTE_ARGS_METADATA, constructor, propertyName as string);
descriptor.value = function (...args: any[]) {
const result = originalMethod.apply(this, args);
//TODO : retrieve the request / contextExecution
//TODO : pass the request / contextExecution to children functions...
return result;
};
Reflect.defineProperty(constructor.prototype, propertyName, descriptor);
Reflect.defineMetadata(ROUTE_ARGS_METADATA, routeArgsMetada, constructor, propertyName as string);
}
};
This #Loggable() decorator would be attached to all injectable classes that need to log or throw execution context
Is that possible ? If not why ?
PS: I'm wondering, how could the #Guard annotation get the context? and how could the #Req annotations get the request?
https://github.com/nestjs/nest/tree/master/packages/common/decorators/http
https://github.com/nestjs/nest/blob/master/packages/common/decorators/core/use-guards.decorator.ts
How #Req does get the Request?
Download source of NestJS from here: https://github.com/nestjs/nest
and look for 'RouteParamtypes.REQUEST' in TS files. You will find them here:
route-params.decorator.ts
route-params-factory.ts
As you can see decorators generally don't do too much. They just add some metadata to classes, methods, and arguments. All the rest do the framework.
Here #Req only creates a special parameter decorator during startup which is processed by RouteParamsFactory before calling a method.
export const Request: () => ParameterDecorator = createRouteParamDecorator(
RouteParamtypes.REQUEST,
);
So Request is not retrieved by the #Req decorator itself. It only asks the NestJS framework to fill the annotated method parameter with reference of Request before calling the method.
BTW I also struggling with the same problem as you. I also was looking for a solution on how to access ExecutionContext from decorators. But decorators can access only annotated targets (classes, handlers, arguments, ...)
I think ExecutionContext only can be accessed directly from:
pipes
guards
interceptors
or from argument decorators this way:
https://docs.nestjs.com/custom-decorators#param-decorators
import { createParamDecorator, ExecutionContext } from '#nestjs/common';
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
NOTE: You can find source of createParamDecorator() in create-route-param-metadata.decorator.ts .

NestJS - async operation inside error filter

In our NestJS-app we've set up a custom error filter, that catches a certain type of error. For those errors we need to perform a request to elasticsearch in order to log the corresponding error information. Since the elasticsearch request is async I've defined the catch method async:
#Catch(MyExceptionType)
#Injectable()
export class MyExceptionFilter implements ExceptionFilter {
constructor(private readonly elasticsearchService: ElasticsearchService) { }
async catch(exception: MyExceptionType, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest<MyRequestModel>();
const response = ctx.getResponse<MyResponseModel>();
const elasticSearchPayload = PayloadBuilder.of(request, exception);
await this.elasticsearchService.report(elasticSearchPayload);
// ...
response.status(exception.getStatus()).json({...});
}
}
Now - so far this works fine, but I'm wondering if this is actually ok to do, as the ExceptionFilter interface strictly declares catch to be a synchronous method.
Could we run into trouble doing this?
ExceptionFilters are to define your error handling logic. I don't think it should be an issue having it async, Nest just won't wait for the logic to finish, however, it shouldn't invoke any other exception handlers due to how it's custom filter code is written.

NestJS handle service exceptions

I'm working on a NestJS app where my services are not always called by a controller or any http request at all. Rather some services are called by a cron schedule to periodically fetch data.
What would be the best way to handle errors in this scenario? I implemented a "catch-all" exception filter, but when my service is called "internally" (not by a controller/request), there error does not get caught and I have an uncaught promise error.
See my question here: Use global nest module in decorator
This decorator catches errors of a class method and logs them. The logging part is not necessary, you could implement your own error handling logic.
import { Inject } from '#nestjs/common';
import { LoggerService } from '../../logger/logger.service';
export function logErrorDecorator(bubble = false) {
const injectLogger = Inject(LoggerService);
return (target: any, propertyKey: string, propertyDescriptor: PropertyDescriptor) => {
injectLogger(target, 'logger'); // this is the same as using constructor(private readonly logger: LoggerService) in a class
//get original method
const originalMethod = propertyDescriptor.value;
//redefine descriptor value within own function block
propertyDescriptor.value = async function(...args: any[]) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
const logger: LoggerService = this.logger;
logger.setContext(target.constructor.name);
logger.error(error.message, error.stack);
// rethrow error, so it can bubble up
if (bubble) {
throw error;
}
}
};
};
}
With this decorator you can simply add the logErrorDecorator() to your service class methods

Resources