From 7fc62bb5c991accbfb683bef25d896672b742206 Mon Sep 17 00:00:00 2001 From: Aramik Date: Mon, 9 Sep 2024 17:07:30 -0700 Subject: [PATCH] account-api: added timeout and body size interceptors --- .prettierignore | 1 + apps/account-api/src/main.ts | 10 ++++++-- developer-docs/account/ENVIRONMENT.md | 2 ++ env-files/account.template.env | 8 ++++++- .../src/config/config.service.spec.ts | 15 ++++++++++++ libs/account-lib/src/config/config.service.ts | 10 ++++++++ libs/account-lib/src/config/env.config.ts | 2 ++ .../src/utils/timeout.interceptor.ts | 24 +++++++++++++++++++ 8 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 libs/account-lib/src/utils/timeout.interceptor.ts diff --git a/.prettierignore b/.prettierignore index f4022465..9a95abe4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -13,3 +13,4 @@ services/*/docs/index.html openapi-specs/* **/target/** jest.config.json +docs/* \ No newline at end of file diff --git a/apps/account-api/src/main.ts b/apps/account-api/src/main.ts index 3c0b6f02..893d3d26 100644 --- a/apps/account-api/src/main.ts +++ b/apps/account-api/src/main.ts @@ -5,6 +5,8 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { ConfigService } from '#account-lib/config/config.service'; import { initSwagger } from '#account-lib/config/swagger_config'; import { ApiModule } from './api.module'; +import { TimeoutInterceptor } from '#account-lib/utils/timeout.interceptor'; +import { NestExpressApplication } from '@nestjs/platform-express'; const logger = new Logger('main'); @@ -30,8 +32,9 @@ async function bootstrap() { process.exit(1); }); - const app = await NestFactory.create(ApiModule, { + const app = await NestFactory.create(ApiModule, { logger: process.env.DEBUG ? ['error', 'warn', 'log', 'verbose', 'debug'] : ['error', 'warn', 'log'], + rawBody: true, }); // Get event emitter & register a shutdown listener @@ -43,10 +46,13 @@ async function bootstrap() { }); try { + const configService = app.get(ConfigService); + app.enableShutdownHooks(); app.useGlobalPipes(new ValidationPipe()); + app.useGlobalInterceptors(new TimeoutInterceptor(configService.apiTimeoutMs)); + app.useBodyParser('json', { limit: configService.apiBodyJsonLimit }); - const configService = app.get(ConfigService); await initSwagger(app, '/docs/swagger'); logger.log(`Listening on port ${configService.apiPort}`); await app.listen(configService.apiPort); diff --git a/developer-docs/account/ENVIRONMENT.md b/developer-docs/account/ENVIRONMENT.md index 3de0a2ce..b4504278 100644 --- a/developer-docs/account/ENVIRONMENT.md +++ b/developer-docs/account/ENVIRONMENT.md @@ -23,3 +23,5 @@ This application recognizes the following environment variables: | `WEBHOOK_FAILURE_THRESHOLD` | Number of failures allowed in the provider webhook before the service is marked down | > 0 | | 3 | | `WEBHOOK_RETRY_INTERVAL_SECONDS` | Number of seconds between provider webhook retry attempts when failing | > 0 | | 10 | | `GRAPH_ENVIRONMENT_TYPE` | Graph environment type. | Mainnet\|TestnetPaseo | Y | | +| `API_TIMEOUT_MS` | Api timeout limit in milliseconds | > 0 | | 5000 | +| `API_BODY_JSON_LIMIT` | Api json body size limit in string (some examples: 100kb or 5mb or etc) | string | | 1mb | diff --git a/env-files/account.template.env b/env-files/account.template.env index 89528acf..65c04f1b 100644 --- a/env-files/account.template.env +++ b/env-files/account.template.env @@ -74,4 +74,10 @@ CACHE_KEY_PREFIX=account: # Graph Sdk environment GRAPH_ENVIRONMENT_TYPE=TestnetPaseo -#GRAPH_ENVIRONMENT_TYPE=Mainnet \ No newline at end of file +#GRAPH_ENVIRONMENT_TYPE=Mainnet + +# Api timeout limit in milliseconds +API_TIMEOUT_MS=5000 + +# Api json body size limit in string (some examples: 100kb or 5mb or etc) +API_BODY_JSON_LIMIT=1mb \ No newline at end of file diff --git a/libs/account-lib/src/config/config.service.spec.ts b/libs/account-lib/src/config/config.service.spec.ts index 11fa57de..fc9b3fdb 100644 --- a/libs/account-lib/src/config/config.service.spec.ts +++ b/libs/account-lib/src/config/config.service.spec.ts @@ -54,6 +54,8 @@ describe('AccountServiceConfig', () => { HEALTH_CHECK_MAX_RETRIES: undefined, CAPACITY_LIMIT: undefined, CACHE_KEY_PREFIX: undefined, + API_TIMEOUT_MS: undefined, + API_BODY_JSON_LIMIT: undefined, }; beforeAll(() => { @@ -121,6 +123,11 @@ describe('AccountServiceConfig', () => { await expect(setupConfigService({ CAPACITY_LIMIT: undefined, ...env })).rejects.toBeDefined(); }); + it('invalid api timeout limit should fail', async () => { + const { API_TIMEOUT_MS: dummy, ...env } = ALL_ENV; + await expect(setupConfigService({ API_TIMEOUT_MS: 0, ...env })).rejects.toBeDefined(); + }); + it('invalid capacity limit should fail', async () => { const { CAPACITY_LIMIT: dummy, ...env } = ALL_ENV; await expect( @@ -247,5 +254,13 @@ describe('AccountServiceConfig', () => { it('should get cache key prefix', () => { expect(accountServiceConfig.cacheKeyPrefix).toStrictEqual(ALL_ENV.CACHE_KEY_PREFIX?.toString()); }); + + it('should get api timeout limit milliseconds', () => { + expect(accountServiceConfig.apiTimeoutMs).toStrictEqual(parseInt(ALL_ENV.API_TIMEOUT_MS as string, 10)); + }); + + it('should get api json body size limit', () => { + expect(accountServiceConfig.apiBodyJsonLimit).toStrictEqual(ALL_ENV.API_BODY_JSON_LIMIT?.toString()); + }); }); }); diff --git a/libs/account-lib/src/config/config.service.ts b/libs/account-lib/src/config/config.service.ts index c87790dc..a3f81a3e 100644 --- a/libs/account-lib/src/config/config.service.ts +++ b/libs/account-lib/src/config/config.service.ts @@ -26,6 +26,8 @@ export interface ConfigEnvironmentVariables { HEALTH_CHECK_MAX_RETRIES: number; CAPACITY_LIMIT: string; CACHE_KEY_PREFIX: string; + API_TIMEOUT_MS: number; + API_BODY_JSON_LIMIT: string; } /// Config service to get global app and provider-specific config values. @@ -174,4 +176,12 @@ export class ConfigService implements OnModuleInit { public get graphEnvironmentType(): keyof EnvironmentType { return this.nestConfigService.get('GRAPH_ENVIRONMENT_TYPE')!; } + + public get apiTimeoutMs(): number { + return this.nestConfigService.get('API_TIMEOUT_MS') ?? 5000; + } + + public get apiBodyJsonLimit(): string { + return this.nestConfigService.get('API_BODY_JSON_LIMIT') ?? '1mb'; + } } diff --git a/libs/account-lib/src/config/env.config.ts b/libs/account-lib/src/config/env.config.ts index e2ac57b0..9a620262 100644 --- a/libs/account-lib/src/config/env.config.ts +++ b/libs/account-lib/src/config/env.config.ts @@ -88,5 +88,7 @@ export const configModuleOptions = (allowReadOnly: boolean): ConfigModuleOptions return value; }) .required(), + API_TIMEOUT_MS: Joi.number().min(1).default(5000), + API_BODY_JSON_LIMIT: Joi.string().default('1mb'), }), }); diff --git a/libs/account-lib/src/utils/timeout.interceptor.ts b/libs/account-lib/src/utils/timeout.interceptor.ts new file mode 100644 index 00000000..caf594c7 --- /dev/null +++ b/libs/account-lib/src/utils/timeout.interceptor.ts @@ -0,0 +1,24 @@ +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 { + timeoutMs: number; + + constructor(timeoutMs: number) { + this.timeoutMs = timeoutMs; + } + + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next.handle().pipe( + timeout(this.timeoutMs), + catchError((err) => { + if (err instanceof TimeoutError) { + return throwError(() => new RequestTimeoutException()); + } + return throwError(() => err); + }), + ); + } +}