diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index bd76ef49b4..ddda044a79 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -1,4 +1,5 @@ import { EventsModule } from '@ghostfolio/api/events/events.module'; +import { PortfolioSnapshotComputationExceptionFilter } from '@ghostfolio/api/filters/portfolio-snapshot-computation-exception.filter'; import { getRedisConnectionOptions } from '@ghostfolio/api/helper/redis.helper'; import { BullBoardAuthMiddleware } from '@ghostfolio/api/middlewares/bull-board-auth.middleware'; import { HtmlTemplateMiddleware } from '@ghostfolio/api/middlewares/html-template.middleware'; @@ -24,6 +25,7 @@ import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis' import { BullModule } from '@nestjs/bull'; import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { APP_FILTER } from '@nestjs/core'; import { EventEmitterModule } from '@nestjs/event-emitter'; import { ScheduleModule } from '@nestjs/schedule'; import { ServeStaticModule } from '@nestjs/serve-static'; @@ -184,7 +186,13 @@ import { UserModule } from './user/user.module'; UserModule, WatchlistModule ], - providers: [I18nService] + providers: [ + I18nService, + { + provide: APP_FILTER, + useClass: PortfolioSnapshotComputationExceptionFilter + } + ] }) export class AppModule implements NestModule { public configure(consumer: MiddlewareConsumer) { diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index fd73bc73d2..cdab3fdf06 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -1,4 +1,5 @@ import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service'; +import { PortfolioSnapshotComputationError } from '@ghostfolio/api/app/portfolio/errors/portfolio-snapshot-computation.error'; import { PortfolioCalculatorPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-position.interface'; import { PortfolioOrder } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-order.interface'; import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface'; @@ -1190,9 +1191,9 @@ export abstract class PortfolioCalculator { }); } } else { - if (attempt >= PortfolioCalculator.MAX_INITIALIZATION_ATTEMPTS) { - throw new Error( - `Portfolio snapshot of user '${this.userId}' could not be computed after ${attempt} attempts` + if (attempt > PortfolioCalculator.MAX_INITIALIZATION_ATTEMPTS) { + throw new PortfolioSnapshotComputationError( + `Portfolio snapshot of user '${this.userId}' could not be computed after ${PortfolioCalculator.MAX_INITIALIZATION_ATTEMPTS} attempts` ); } diff --git a/apps/api/src/app/portfolio/errors/portfolio-snapshot-computation.error.ts b/apps/api/src/app/portfolio/errors/portfolio-snapshot-computation.error.ts new file mode 100644 index 0000000000..074ac0bae7 --- /dev/null +++ b/apps/api/src/app/portfolio/errors/portfolio-snapshot-computation.error.ts @@ -0,0 +1,7 @@ +export class PortfolioSnapshotComputationError extends Error { + public constructor(message: string) { + super(message); + + this.name = 'PortfolioSnapshotComputationError'; + } +} diff --git a/apps/api/src/filters/portfolio-snapshot-computation-exception.filter.ts b/apps/api/src/filters/portfolio-snapshot-computation-exception.filter.ts new file mode 100644 index 0000000000..05471c3f60 --- /dev/null +++ b/apps/api/src/filters/portfolio-snapshot-computation-exception.filter.ts @@ -0,0 +1,26 @@ +import { PortfolioSnapshotComputationError } from '@ghostfolio/api/app/portfolio/errors/portfolio-snapshot-computation.error'; + +import { ArgumentsHost, Catch, ExceptionFilter, Logger } from '@nestjs/common'; +import { Response } from 'express'; +import { getReasonPhrase, StatusCodes } from 'http-status-codes'; + +@Catch(PortfolioSnapshotComputationError) +export class PortfolioSnapshotComputationExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger( + PortfolioSnapshotComputationExceptionFilter.name + ); + + public catch( + exception: PortfolioSnapshotComputationError, + host: ArgumentsHost + ) { + this.logger.error(exception.message); + + const response = host.switchToHttp().getResponse(); + + response.status(StatusCodes.SERVICE_UNAVAILABLE).json({ + message: getReasonPhrase(StatusCodes.SERVICE_UNAVAILABLE), + statusCode: StatusCodes.SERVICE_UNAVAILABLE + }); + } +} diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts index 855721159f..fddbd01aba 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock.ts @@ -8,13 +8,13 @@ import { setTimeout } from 'timers/promises'; import { PortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queue-job.interface'; export const PortfolioSnapshotServiceMock = { - addJobToQueue({ + addJobToQueue: ({ opts }: { data: PortfolioSnapshotQueueJob; name: string; opts?: JobOptions; - }): Promise { + }): Promise => { const mockJob: Partial = { finished: async () => { await setTimeout(100); @@ -31,17 +31,17 @@ export const PortfolioSnapshotServiceMock = { } }; - this.jobsStore.set(opts?.jobId, mockJob); + PortfolioSnapshotServiceMock.jobsStore.set(opts?.jobId, mockJob); return Promise.resolve(mockJob as Job); }, - getJob(jobId: JobId): Promise { - const job = this.jobsStore.get(jobId); + getJob: (jobId: JobId): Promise => { + const job = PortfolioSnapshotServiceMock.jobsStore.get(jobId); return Promise.resolve(job as Job); }, jobsStore: new Map>(), - reset() { - this.jobsStore.clear(); + reset: () => { + PortfolioSnapshotServiceMock.jobsStore.clear(); } };