diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 696565c..1554713 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -8,6 +8,7 @@ import { VaccinationModule } from './vaccination/vaccination.module'; import { NipModule } from './nip/nip.module'; import { PasswordResetModule } from './password-reset/password-reset.module'; import { JsonRpcModule } from './json-rpc/json-rpc.module'; +import { DomainExceptionFilter } from './exception-filter'; @Module({ imports: [ @@ -22,6 +23,7 @@ import { JsonRpcModule } from './json-rpc/json-rpc.module'; PasswordResetModule, JsonRpcModule.forRoot(), ], + providers: [DomainExceptionFilter], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { diff --git a/apps/server/src/exception-filter.ts b/apps/server/src/exception-filter.ts index 2a4f7ea..a5f9499 100644 --- a/apps/server/src/exception-filter.ts +++ b/apps/server/src/exception-filter.ts @@ -1,17 +1,15 @@ -import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; import { DomainException } from './exception/domain-exception'; -import { Request, Response } from 'express'; -import { ErrorResponse } from './common/error-response'; +import { JsonRpcExceptionFilter } from './json-rpc/json-rpc.decorator'; +import { ExceptionFilter } from './json-rpc/exceptions/exception-filter'; -@Catch(DomainException) -export class DomainExceptionFilter implements ExceptionFilter { - catch(exception: DomainException, host: ArgumentsHost) { - const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - const request = ctx.getRequest(); - - response - .status(400) - .json(ErrorResponse.of(exception.errorData, exception.errorData.message)); +@JsonRpcExceptionFilter(DomainException) +export class DomainExceptionFilter implements ExceptionFilter { + catch(exception: DomainException, callback: any) { + callback({ + code: -100, + message: exception.errorData.message, + errorData: exception.errorData, + data: exception.data, + }); } } diff --git a/apps/server/src/json-rpc/default-exception-filter.ts b/apps/server/src/json-rpc/default-exception-filter.ts new file mode 100644 index 0000000..53567d4 --- /dev/null +++ b/apps/server/src/json-rpc/default-exception-filter.ts @@ -0,0 +1,15 @@ +import { JsonRpcExceptionFilter } from './json-rpc.decorator'; +import { JsonRpcError } from './error'; +import { ExceptionFilter } from './exceptions/exception-filter'; +import { JSONRPCCallbackType } from 'jayson'; + +@JsonRpcExceptionFilter(Error) +export class JsonRpcDefaultExceptionFilter implements ExceptionFilter { + catch(exception: Error, callback: JSONRPCCallbackType): void { + if (exception instanceof JsonRpcError) { + callback(exception); + } else { + throw exception; + } + } +} diff --git a/apps/server/src/json-rpc/exceptions/exception-filter.ts b/apps/server/src/json-rpc/exceptions/exception-filter.ts new file mode 100644 index 0000000..029f9de --- /dev/null +++ b/apps/server/src/json-rpc/exceptions/exception-filter.ts @@ -0,0 +1,8 @@ +import * as jayson from 'jayson'; + +export interface ExceptionFilter { + catch( + exception: T, + callback: jayson.JSONRPCCallbackType + ): Promise | void; +} diff --git a/apps/server/src/json-rpc/exceptions/exception-handler.ts b/apps/server/src/json-rpc/exceptions/exception-handler.ts new file mode 100644 index 0000000..e6b026b --- /dev/null +++ b/apps/server/src/json-rpc/exceptions/exception-handler.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { ExceptionFilter } from './exception-filter'; +import { JSONRPCCallbackType } from 'jayson'; + +export type JsonRpcExceptionHandlerMap = { + [exceptionType: string]: ExceptionFilter; +}; + +@Injectable() +export class JsonRpcExceptionHandler { + private exceptionFilters: JsonRpcExceptionHandlerMap = {}; + + constructor() {} + + registerFilter(exception: string, filter: ExceptionFilter): void { + this.exceptionFilters[exception] = filter; + } + + handle(exception: T, callback: JSONRPCCallbackType): void { + const exceptionType = Object.getPrototypeOf(exception).constructor.name; + const filter = this.exceptionFilters[exceptionType]; + + if (!filter) { + throw exception; + } + + try { + filter.catch(exception, callback); + } catch (e) { + callback(e, e); + } + } +} diff --git a/apps/server/src/json-rpc/json-rpc.decorator.ts b/apps/server/src/json-rpc/json-rpc.decorator.ts index 39c9a0f..30a05f5 100644 --- a/apps/server/src/json-rpc/json-rpc.decorator.ts +++ b/apps/server/src/json-rpc/json-rpc.decorator.ts @@ -1,8 +1,9 @@ -import { SetMetadata } from '@nestjs/common'; +import { applyDecorators, Injectable, SetMetadata } from '@nestjs/common'; export const JSON_RPC_CONTROLLER = Symbol('JSON_RPC_CONTROLLER'); export const JSON_RPC_METHOD = Symbol('JSON_RPC_METHOD'); export const JSON_RPC_PARAMS = Symbol('JSON_RPC_PARAMS'); +export const JSON_RPC_EXCEPTION_FILTER = Symbol('JSON_RPC_EXCEPTION_FILTER'); export function JsonRpcController(controllerName: string): ClassDecorator { return SetMetadata(JSON_RPC_CONTROLLER, controllerName); @@ -12,6 +13,13 @@ export function JsonRpcMethod(methodName: string) { return SetMetadata(JSON_RPC_METHOD, methodName); } +export function JsonRpcExceptionFilter(exception: T): ClassDecorator { + return applyDecorators( + SetMetadata(JSON_RPC_EXCEPTION_FILTER, exception), + Injectable + ); +} + export type RequestContext = { req: Request; body: any; diff --git a/apps/server/src/json-rpc/json-rpc.module.ts b/apps/server/src/json-rpc/json-rpc.module.ts index 588d3c5..f2e2df5 100644 --- a/apps/server/src/json-rpc/json-rpc.module.ts +++ b/apps/server/src/json-rpc/json-rpc.module.ts @@ -8,17 +8,29 @@ import { import { DiscoveryModule } from '@nestjs/core'; import { JsonRpcService } from './json-rpc.service'; import { LoggerMiddleware } from './middleware'; +import { JsonRpcDefaultExceptionFilter } from './default-exception-filter'; +import { JsonRpcExceptionHandler } from './exceptions/exception-handler'; @Module({ imports: [DiscoveryModule], - providers: [JsonRpcService, Logger], + providers: [ + JsonRpcService, + Logger, + JsonRpcDefaultExceptionFilter, + JsonRpcExceptionHandler, + ], }) export class JsonRpcModule implements NestModule { static forRoot(): DynamicModule { return { global: true, module: JsonRpcModule, - providers: [JsonRpcService, Logger], + providers: [ + JsonRpcService, + Logger, + JsonRpcDefaultExceptionFilter, + JsonRpcExceptionHandler, + ], }; } diff --git a/apps/server/src/json-rpc/json-rpc.service.ts b/apps/server/src/json-rpc/json-rpc.service.ts index 704e5ac..a5ae62f 100644 --- a/apps/server/src/json-rpc/json-rpc.service.ts +++ b/apps/server/src/json-rpc/json-rpc.service.ts @@ -3,6 +3,7 @@ import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core'; import * as jayson from 'jayson'; import { JSON_RPC_CONTROLLER, + JSON_RPC_EXCEPTION_FILTER, JSON_RPC_METHOD, JSON_RPC_PARAMS, JsonRpcParam, @@ -10,8 +11,9 @@ import { RequestContext, } from './json-rpc.decorator'; import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; +import { ExceptionFilter } from './exceptions/exception-filter'; +import { JsonRpcExceptionHandler } from './exceptions/exception-handler'; import { JsonRpcError } from './error'; -import { DomainException } from '../exception/domain-exception'; @Injectable() export class JsonRpcService implements OnModuleInit { @@ -20,6 +22,7 @@ export class JsonRpcService implements OnModuleInit { constructor( private metadataScanner: MetadataScanner, private discoveryService: DiscoveryService, + private exceptionHandler: JsonRpcExceptionHandler, private reflector: Reflector, private logger: Logger ) {} @@ -60,7 +63,11 @@ export class JsonRpcService implements OnModuleInit { this.server.method( handlerName, - async (params: any | any[], context: any, callback: any) => { + async ( + params: any, + context: RequestContext, + callback: jayson.JSONRPCCallbackType + ) => { try { const result = await methodRef.apply( instance, @@ -69,23 +76,45 @@ export class JsonRpcService implements OnModuleInit { body: params, }) ); + callback(null, result); } catch (error) { - if (error instanceof JsonRpcError) { - return callback(error); - } - if (error instanceof DomainException) { - return callback(new JsonRpcError(-32001, error.message, error)); + try { + this.exceptionHandler.handle(error, callback); + } catch (e) { + callback(new JsonRpcError(-32603, e.message, e.data)); } - - console.log(error); - - return callback(new JsonRpcError(-32000, 'Internal server error')); } } ); } + registerExceptionFilters() { + this.discoveryService + .getProviders() + .filter((wrapper) => wrapper.isDependencyTreeStatic()) + .filter((wrapper) => wrapper.instance) + .forEach((provider: InstanceWrapper) => { + const { instance } = provider; + + const exception: Error | undefined = Reflect.getOwnMetadata( + JSON_RPC_EXCEPTION_FILTER, + instance.constructor + ); + + if (!exception) { + return; + } + + const exceptionFilter = instance as ExceptionFilter; + this.exceptionHandler.registerFilter(exception.name, exceptionFilter); + this.logger.log( + `Registered exception filter ${exception.name}`, + JsonRpcService.name + ); + }); + } + registerMethods() { this.discoveryService .getProviders() @@ -126,5 +155,6 @@ export class JsonRpcService implements OnModuleInit { onModuleInit(): any { this.registerMethods(); + this.registerExceptionFilters(); } } diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 59f6af6..ff409c4 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -1,10 +1,8 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; -import { DomainExceptionFilter } from './exception-filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); - app.useGlobalFilters(new DomainExceptionFilter()); await app.listen(3000); }