Nestjs | grpc : How to handle remote error from client side - node.js

Remote Server
#Catch(RpcException)
export class RpcExceptionHandler implements RpcExceptionFilter<RpcException> {
catch(exception: RpcException, host: ArgumentsHost): Observable<any> {
return throwError(exception.getError());
}
}
#UseFilters(new RpcExceptionHandler())
#GrpcMethod('AppController', 'Accumulate')
async accumulate(numberArray: INumberArray, metadata: any): Promise<ISumOfNumberArray> {
throw new RpcException({
code: 5,
message: 'Data Not Found'
})
}
Client code
#Get('add')
async getSumc(#Query('data') data: number[]) {
try {
let ata = await this.grpcService.accumulate({ data });
return ata;
} catch (err) {
//logic here if error comes
return err;
}
}
Proto defination.
syntax = "proto3";
package app;
// Declare a service for each controller you have
service AppController {
// Declare an rpc for each method that is called via gRPC
rpc Accumulate (NumberArray) returns (SumOfNumberArray);
}
// Declare the types used above
message NumberArray {
repeated double data = 1;
}
message SumOfNumberArray {
double sum = 1;
}
If error comes it is not going to catch block, just showing the server error.
I want to catch the error if remote throwing any error.

Try this one:
#Get('add')
async getSumc(#Query('data') data: number[]) {
try {
let ata = await this.grpcService.accumulate({ data }).toPromise();
return ata;
} catch (e) {
throw new RpcException(e);
}
}
Example here

Related

In nestjs, how can we change default error messages from typeORM globally?

I have this code to change the default message from typeorm when a value in a unique column already exists. It just creates a custom message when we get an error 23505.
if (error.code === '23505') {
// message = This COLUMN VALUE already exists.
const message = error.detail.replace(
/^Key \((.*)\)=\((.*)\) (.*)/,
'The $1 $2 already exists.',
);
throw new BadRequestException(message);
}
throw new InternalServerErrorException();
I will have to use it in other services, so I would like to abstract that code.
I think I could just create a helper and then I import and call it wherever I need it. But I don’t know if there is a better solution to use it globally with a filter or an interceptor, so I don’t have to even import and call it in different services.
Is this possible? how can that be done?
If it is not possible, what do you think the best solution would be?
Here all the service code:
#Injectable()
export class MerchantsService {
constructor(
#InjectRepository(Merchant)
private merchantRepository: Repository<Merchant>,
) {}
public async create(createMerchantDto: CreateMerchantDto) {
try {
const user = this.merchantRepository.create({
...createMerchantDto,
documentType: DocumentType.NIT,
isActive: false,
});
await this.merchantRepository.save(user);
const { password, ...merchantData } = createMerchantDto;
return {
...merchantData,
};
} catch (error) {
if (error.code === '23505') {
// message = This COLUMN VALUE already exists.
const message = error.detail.replace(
/^Key \((.*)\)=\((.*)\) (.*)/,
'The $1 $2 already exists.',
);
throw new BadRequestException(message);
}
throw new InternalServerErrorException();
}
}
public async findOneByEmail(email: string): Promise<Merchant | null> {
return this.merchantRepository.findOneBy({ email });
}
}
I created an exception filter for typeORM errors.
This was the result:
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpStatus,
InternalServerErrorException,
} from '#nestjs/common';
import { Response } from 'express';
import { QueryFailedError, TypeORMError } from 'typeorm';
type ExceptionResponse = {
statusCode: number;
message: string;
};
#Catch(TypeORMError, QueryFailedError)
export class TypeORMExceptionFilter implements ExceptionFilter {
private defaultExceptionResponse: ExceptionResponse =
new InternalServerErrorException().getResponse() as ExceptionResponse;
private exceptionResponse: ExceptionResponse = this.defaultExceptionResponse;
catch(exception: TypeORMError | QueryFailedError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
exception instanceof QueryFailedError &&
this.setQueryFailedErrorResponse(exception);
response
.status(this.exceptionResponse.statusCode)
.json(this.exceptionResponse);
}
private setQueryFailedErrorResponse(exception: QueryFailedError): void {
const error = exception.driverError;
if (error.code === '23505') {
const message = error.detail.replace(
/^Key \((.*)\)=\((.*)\) (.*)/,
'The $1 $2 already exists.',
);
this.exceptionResponse = {
statusCode: HttpStatus.BAD_REQUEST,
message,
};
}
// Other error codes can be handled here
}
// Add more methods here to set a different response for any other typeORM error, if needed.
// All typeORM erros: https://github.com/typeorm/typeorm/tree/master/src/error
}
I set it globally:
import { TypeORMExceptionFilter } from './common';
async function bootstrap() {
//...Other code
app.useGlobalFilters(new TypeORMExceptionFilter());
//...Other code
await app.listen(3000);
}
bootstrap();
And now I don't have to add any code when doing changes in the database:
#Injectable()
export class MerchantsService {
constructor(
#InjectRepository(Merchant)
private merchantRepository: Repository<Merchant>,
) {}
public async create(createMerchantDto: CreateMerchantDto) {
const user = this.merchantRepository.create({
...createMerchantDto,
documentType: DocumentType.NIT,
isActive: false,
});
await this.merchantRepository.save(user);
const { password, ...merchantData } = createMerchantDto;
return {
...merchantData,
};
}
}
Notice that now I don't use try catch because nest is handling the exceptions. When the repository save() method returns an error (actually it is a rejected promise), it is caught in the filter.

Problem trying to show error message with prism and node

I'm doing the backend of a ToDo's project, and for that I'm using the prism orm, the node framework, the typescript language and to test the project I'm using insomnia. i already did all the methods i wanted (create ToDo, get ToDo's, delete ToDo, change ToDo task, change ToDo title) and they are all working, but when i went to test the error message that would occur when i tried to create a ToDo that already exists, in insomia it gives the following message:
Error: Server returned nothing (no headers, no data)
And in VsCode:
[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "#<AppError>".] {
code: 'ERR_UNHANDLED_REJECTION'
}
I made the code as follows:
export class AppError {
public readonly message: string;
public readonly statusCode: number;
constructor(message: string, statusCode = 400) {
this.message = message
this.statusCode = statusCode
}
}
import { AppError } from "../../errors/AppError";
import { prisma } from "../../client/prismaClient";
import { CreateToDoType } from "../Types/CreateToDoType";
export class createToDoUseCase {
async execute({title, task}: CreateToDoType) {
const todoAlreadyExists = await prisma.toDo.findUnique({
where: {
title: title
}
})
if (!todoAlreadyExists) {
const newToDo = await prisma.toDo.create({
data: {
title: title,
task: task
}
})
return newToDo
} else {
console.log("01 4")
throw new AppError("Error! ToDo already exists") //aqui está o erro
}
}
}
import {Request, Response} from "express"
import { createToDoUseCase } from "../UseCases/CreateToDoUseCases"
export class createToDoController {
async handle(req: Request, res: Response) {
const { title, task } = req.body
const CreateToDoUseCases = new createToDoUseCase()
const result = await CreateToDoUseCases.execute({title, task}) // aqui está o erro
return res.status(201).json(result)
}
}

How can i get many details as possible about error using React Error boundry get error details?

Im tring to catch more details when error accuring.
when the error is accuring im using node api server to catch the error and save in log file.
i simulated a Network Error and tring to get many details as possible.
when i console.log the error in frontend im getting this:
withFormHandler.js:28 Uncaught Error: Error: Network Error
at Object.componentDidUpdate (withFormHandler.js:28)
...
but i cant send this information using my api.
all im getting in the server side is an empty object.
so how can i catch and send many details as possible about the error and write it to the log file?
this is my ErrorBoundry component:
class ErrorBoundary extends React.Component {
state = {
hasError: false,
error: { message: "", stack: "" },
info: { componentStack: "" },
};
static getDerivedStateFromError = (error) => {
return { hasError: true };
};
componentDidCatch = (error, info) => {
this.setState({ error, info });
axios
.get(`http://localhost:5001/api/logs/error`, {
params: {
error: error.message,
details: info,
// details:error ---> this is also not give me information i need
},
})
.catch(() => {});
};
render() {
const { hasError, error, info } = this.state;
const { children } = this.props;
return hasError ? <ErrorComponent message={error.message} /> : children;
}
}
this is the server side handle:
router.get("/error", (req, res) => {
const errorMessage = req.query.error;
const details = req.query.details; -----> return an empty object :(
const logFile = "./logs/debug.log";
if (errorMessage) {
let error = errorMessage + "\r\n" + details;
fs.appendFile(logFile, error, function (err) {
// callback or something
});
}
});

Is it possible to override the DEFAULT_TEARDOWN function inside exceptions-zone.ts?

I'm trying to create an application with NestJS framework and I'd like to check if a specific Service is in the application context but, the framework default behavior is exiting the process when it doesn't find the Service inside the context.
I've surrounded the get method with try...catch but it doesn't work because of the process.exit(1) execution.
private async loadConfigService(server: INestApplication): Promise<void> {
try {
this.configService = await server.get<ConfigService>(ConfigService, { strict: false });
} catch (error) {
this.logger.debug('Server does not have a config module loaded. Loading defaults...');
}
this.port = this.configService ? this.configService.port : DEFAULT_PORT;
this.environment = this.configService ? this.configService.environment : Environment[process.env.NODE_ENV] || Environment.DEVELOPMENT;
this.isProduction = this.configService ? this.configService.isProduction : false;
}
I'd like to catch the exception to manage the result instead of exiting the process.
Here's what I came up with:
import { NestFactory } from '#nestjs/core';
export class CustomNestFactory {
constructor() {}
public static create(module, serverOrOptions, options) {
const ob = NestFactory as any;
ob.__proto__.createExceptionZone = (receiver, prop) => {
return (...args) => {
const result = receiver[prop](...args);
return result;
};
};
return NestFactory.create(module, serverOrOptions, options);
}
}
Now, instead of using the default NestFactory I can use my CustomNestFactory
Dirty, but it works
Here is my code to solve same issue:
import { ExceptiinsZone } from '#nestjs/core/errors/exceptions-zone';
ExceptionsZone.run = callback => {
try {
callback();
} catch (e) {
ExceptionsZone.exceptionHandler.handle(e);
throw e;
}
};
You may need to override asyncRun() also for other method.

Bidirectional WebSockets in laravel?

Anyone here familiar with bi-directional WebSockets in laravel?
Please note I am not talking about pusher+echo. pusher+echo cannot make a request to the server.
I have experience in express.js in which the socket can listen at server side.
I am looking for a similar feature.. any suggestion or help is greatly appreciated
Have you tried the laravel events method along with socketIO (as discussed in https://laracasts.com/discuss/channels/general-discussion/step-by-step-guide-to-installing-socketio-and-broadcasting-events-with-laravel-51)
In this method you could emit messages back to socket server, or you could initiate an XHR request to directly interact with the backend server (server with laravel).
I use Laravel Broadcasting with Laravel Websockets and Laravel Echo for server to client communication while using the code below for client to server communication using websockets api. Big thanks to Acadea.io. Please refer to the following:
BaseSocketHandler.php
namespace App\Websockets\SocketHandler;
use BeyondCode\LaravelWebSockets\Apps\App;
use BeyondCode\LaravelWebSockets\QueryParameters;
use BeyondCode\LaravelWebSockets\WebSockets\Exceptions\UnknownAppKey;
use Ratchet\ConnectionInterface;
use Ratchet\WebSocket\MessageComponentInterface;
abstract class BaseSocketHandler implements MessageComponentInterface
{
protected function verifyAppKey(ConnectionInterface $connection)
{
$appKey = QueryParameters::create($connection->httpRequest)->get('appKey');
if (!$app = App::findByKey($appKey)) {
throw new UnknownAppKey($appKey);
}
$connection->app = $app;
return $this;
}
protected function generateSocketId(ConnectionInterface $connection)
{
$socketId = sprintf('%d.%d', random_int(1, 1000000000), random_int(1, 1000000000));
$connection->socketId = $socketId;
return $this;
}
function onOpen(ConnectionInterface $conn)
{
dump('on opened');
$this->verifyAppKey($conn)->generateSocketId($conn);
}
function onClose(ConnectionInterface $conn)
{
dump('closed');
}
function onError(ConnectionInterface $conn, \Exception $e)
{
dump($e);
dump('onerror');
}
}
CustomSocketHandler
namespace App\Websockets\SocketHandler;
use Ratchet\ConnectionInterface;
use Ratchet\RFC6455\Messaging\MessageInterface;
use Ratchet\WebSocket\MessageComponentInterface;
class CustomSocketHandler extends BaseSocketHandler implements MessageComponentInterface
{
public function onOpen(ConnectionInterface $connection)
{
// TODO: Implement onOpen() method.
}
public function onClose(ConnectionInterface $connection)
{
// TODO: Implement onClose() method.
}
public function onError(ConnectionInterface $connection, \Exception $e)
{
// TODO: Implement onError() method.
}
public function onMessage(ConnectionInterface $connection, MessageInterface $msg)
{
$body = json_decode(collect(json_decode($msg->getPayload(), true)), true);
$access_token = $body["access_token"];
$authenticateUserByAccessToken = new AuthenticateUserByAccessToken();
$chat_user = $authenticateUserByAccessToken->getAuthenticatedUser($access_token);
// TODO: Implement onMessage() method.
$dataToBeReturned = json_encode($dataToBeReturned);
$connection->send($dataToBeReturned);
}
web
use App\Websockets\SocketHandler\CustomSocketHandler;
use BeyondCode\LaravelWebSockets\Facades\WebSocketsRouter;
WebSocketsRouter::webSocket('/socket/custom-socket-url', CustomSocketHandler::class);
JS
function functionName(access_token, id) {
const socket = new WebSocket(
`wss://${window.location.hostname}:${process.env.MIX_LARAVEL_WEBSOCKETS_PORT}/socket/custom-socket-url?appKey=${process.env.MIX_PUSHER_APP_KEY}`
);
socket.onopen = function (event) {
// console.log("on open", event);
};
socket.onclose = function (event) {
// console.log("on close", event);
};
socket.onmessage = function (event) {
// console.log("on message", event);
};
socket.send(
JSON.stringify({
access_token: access_token, //optional JWT Auth Token
payload: {
id: id,
},
})
);
socket.close();
}
Optional. If you want to authenticate your websocket connection (using jwt)
namespace App\Websockets\SocketHandler;
use App\Models\AuthenticationModelToBeUsed;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Tymon\JWTAuth\Facades\JWTAuth;
class AuthenticateUserByAccessToken
{
public function getAuthenticatedUser($access_token)
{
Config::set('auth.providers.users.model', AuthenticationModelToBeUsed::class);
try {
JWTAuth::setToken($access_token);
if (!$user = JWTAuth::toUser($access_token)) {
return response()->json(array('message' => 'user_not_found'), 404);
}
} catch (\Tymon\JWTAuth\Exceptions\TokenExpiredException $e) {
return response()->json(array('message' => 'token_expired'), 404);
} catch (\Tymon\JWTAuth\Exceptions\TokenInvalidException $e) {
return response()->json(array('message' => 'token_invalid'), 404);
} catch (\Tymon\JWTAuth\Exceptions\JWTException $e) {
return response()->json(array('message' => 'token_absent'), 404);
}
Log::info($user);
return $user;
}
}

Resources