Skip to content

Commit 882bd8f

Browse files
authored
[Recording Oracle] Implement custom error handling (#3342)
* Implement custom error handling and replace existing exceptions with specific error classes * add non-null assertion for privateKey in encryption setup * update exception response format and improve error handling
1 parent f68b8b1 commit 882bd8f

File tree

22 files changed

+213
-187
lines changed

22 files changed

+213
-187
lines changed

packages/apps/fortune/recording-oracle/src/app.module.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Module } from '@nestjs/common';
22
import { ConfigModule } from '@nestjs/config';
3-
import { APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
3+
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
44
import { HttpValidationPipe } from './common/pipes';
55
import { JobModule } from './modules/job/job.module';
66

@@ -10,13 +10,18 @@ import { WebhookModule } from './modules/webhook/webhook.module';
1010
import { envValidator } from './common/config/env-schema';
1111
import { EnvConfigModule } from './common/config/config.module';
1212
import { TransformEnumInterceptor } from './common/interceptors/transform-enum.interceptor';
13+
import { ExceptionFilter } from './common/exceptions/exception.filter';
1314

1415
@Module({
1516
providers: [
1617
{
1718
provide: APP_PIPE,
1819
useClass: HttpValidationPipe,
1920
},
21+
{
22+
provide: APP_FILTER,
23+
useClass: ExceptionFilter,
24+
},
2025
{
2126
provide: APP_INTERCEPTOR,
2227
useClass: SnakeCaseInterceptor,

packages/apps/fortune/recording-oracle/src/common/constants/errors.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export enum ErrorJob {
99
NotFoundIntermediateResultsUrl = 'Error while getting intermediate results url from escrow contract',
1010
SolutionAlreadyExists = 'Solution already exists',
1111
AllSolutionsHaveAlreadyBeenSent = 'All solutions have already been sent',
12-
WebhookWasNotSent = 'Webhook was not sent',
1312
ManifestNotFound = 'Manifest not found',
1413
}
1514

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
export class BaseError extends Error {
2+
constructor(message: string, stack?: string) {
3+
super(message);
4+
this.name = this.constructor.name;
5+
if (stack) {
6+
this.stack = stack;
7+
}
8+
}
9+
}
10+
11+
export class ValidationError extends BaseError {
12+
constructor(message: string, stack?: string) {
13+
super(message, stack);
14+
}
15+
}
16+
17+
export class AuthError extends BaseError {
18+
constructor(message: string, stack?: string) {
19+
super(message, stack);
20+
}
21+
}
22+
23+
export class ForbiddenError extends BaseError {
24+
constructor(message: string, stack?: string) {
25+
super(message, stack);
26+
}
27+
}
28+
29+
export class NotFoundError extends BaseError {
30+
constructor(message: string, stack?: string) {
31+
super(message, stack);
32+
}
33+
}
34+
35+
export class ConflictError extends BaseError {
36+
constructor(message: string, stack?: string) {
37+
super(message, stack);
38+
}
39+
}
40+
41+
export class ServerError extends BaseError {
42+
constructor(message: string, stack?: string) {
43+
super(message, stack);
44+
}
45+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import {
2+
ArgumentsHost,
3+
Catch,
4+
ExceptionFilter as IExceptionFilter,
5+
HttpStatus,
6+
Logger,
7+
} from '@nestjs/common';
8+
import { Request, Response } from 'express';
9+
import {
10+
ValidationError,
11+
AuthError,
12+
ForbiddenError,
13+
NotFoundError,
14+
ConflictError,
15+
ServerError,
16+
} from '../errors';
17+
18+
@Catch()
19+
export class ExceptionFilter implements IExceptionFilter {
20+
private logger = new Logger(ExceptionFilter.name);
21+
private getStatus(exception: any): number {
22+
if (exception instanceof ValidationError) {
23+
return HttpStatus.BAD_REQUEST;
24+
} else if (exception instanceof AuthError) {
25+
return HttpStatus.UNAUTHORIZED;
26+
} else if (exception instanceof ForbiddenError) {
27+
return HttpStatus.FORBIDDEN;
28+
} else if (exception instanceof NotFoundError) {
29+
return HttpStatus.NOT_FOUND;
30+
} else if (exception instanceof ConflictError) {
31+
return HttpStatus.CONFLICT;
32+
} else if (exception instanceof ServerError) {
33+
return HttpStatus.UNPROCESSABLE_ENTITY;
34+
} else if (exception.statusCode) {
35+
return exception.statusCode;
36+
}
37+
return HttpStatus.INTERNAL_SERVER_ERROR;
38+
}
39+
40+
catch(exception: any, host: ArgumentsHost) {
41+
const ctx = host.switchToHttp();
42+
const response = ctx.getResponse<Response>();
43+
const request = ctx.getRequest<Request>();
44+
45+
const status = this.getStatus(exception);
46+
const message = exception.message || 'Internal server error';
47+
48+
this.logger.error(
49+
`Exception caught: ${message}`,
50+
exception.stack || 'No stack trace available',
51+
);
52+
53+
response.status(status).json({
54+
status_code: status,
55+
timestamp: new Date().toISOString(),
56+
message: message,
57+
path: request.url,
58+
});
59+
}
60+
}

packages/apps/fortune/recording-oracle/src/common/filter/global-exceptions.filter.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.

packages/apps/fortune/recording-oracle/src/common/filter/index.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/apps/fortune/recording-oracle/src/common/guards/signature.auth.spec.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Test, TestingModule } from '@nestjs/testing';
2-
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
3-
import { SignatureAuthGuard } from './signature.auth';
4-
import { verifySignature } from '../utils/signature';
51
import { ChainId, EscrowUtils } from '@human-protocol/sdk';
2+
import { ExecutionContext } from '@nestjs/common';
3+
import { Test, TestingModule } from '@nestjs/testing';
64
import { MOCK_ADDRESS } from '../../../test/constants';
7-
import { Role } from '../enums/role';
85
import { HEADER_SIGNATURE_KEY } from '../constants';
6+
import { Role } from '../enums/role';
7+
import { AuthError } from '../errors';
8+
import { verifySignature } from '../utils/signature';
9+
import { SignatureAuthGuard } from './signature.auth';
910

1011
jest.mock('../../common/utils/signature');
1112

@@ -77,18 +78,18 @@ describe('SignatureAuthGuard', () => {
7778
);
7879
});
7980

80-
it('should throw unauthorized exception if signature is not verified', async () => {
81+
it('should throw AuthError if signature is not verified', async () => {
8182
(verifySignature as jest.Mock).mockReturnValue(false);
8283

8384
await expect(guard.canActivate(context as any)).rejects.toThrow(
84-
UnauthorizedException,
85+
AuthError,
8586
);
8687
});
8788

88-
it('should throw unauthorized exception for unrecognized oracle type', async () => {
89+
it('should throw AuthError for unrecognized oracle type', async () => {
8990
mockRequest.originalUrl = '/some/random/path';
9091
await expect(guard.canActivate(context as any)).rejects.toThrow(
91-
UnauthorizedException,
92+
AuthError,
9293
);
9394
});
9495
});

packages/apps/fortune/recording-oracle/src/common/guards/signature.auth.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
import {
2-
CanActivate,
3-
ExecutionContext,
4-
Injectable,
5-
UnauthorizedException,
6-
} from '@nestjs/common';
7-
import { verifySignature } from '../utils/signature';
8-
import { HEADER_SIGNATURE_KEY } from '../constants';
91
import { EscrowUtils } from '@human-protocol/sdk';
2+
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
3+
import { HEADER_SIGNATURE_KEY } from '../constants';
104
import { Role } from '../enums/role';
5+
import { AuthError, ValidationError } from '../errors';
6+
import { verifySignature } from '../utils/signature';
117

128
@Injectable()
139
export class SignatureAuthGuard implements CanActivate {
@@ -42,11 +38,10 @@ export class SignatureAuthGuard implements CanActivate {
4238
if (isVerified) {
4339
return true;
4440
}
45-
} catch (error) {
46-
// eslint-disable-next-line no-console
47-
console.error(error);
41+
} catch (error: any) {
42+
throw new ValidationError(error.message);
4843
}
4944

50-
throw new UnauthorizedException('Unauthorized');
45+
throw new AuthError('Unauthorized');
5146
}
5247
}

packages/apps/fortune/recording-oracle/src/common/interceptors/transform-enum.interceptor.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import {
2+
CallHandler,
3+
ExecutionContext,
24
Injectable,
35
NestInterceptor,
4-
ExecutionContext,
5-
CallHandler,
6-
BadRequestException,
76
} from '@nestjs/common';
8-
import { Observable } from 'rxjs';
9-
import { map } from 'rxjs/operators';
10-
import { plainToInstance, ClassConstructor } from 'class-transformer';
7+
import { ClassConstructor, plainToInstance } from 'class-transformer';
118
import { validateSync } from 'class-validator';
129
import 'reflect-metadata';
10+
import { Observable } from 'rxjs';
11+
import { map } from 'rxjs/operators';
12+
import { ValidationError } from '../errors';
1313

1414
@Injectable()
1515
export class TransformEnumInterceptor implements NestInterceptor {
@@ -72,7 +72,7 @@ export class TransformEnumInterceptor implements NestInterceptor {
7272
// Validate the transformed data
7373
const validationErrors = validateSync(transformedInstance);
7474
if (validationErrors.length > 0) {
75-
throw new BadRequestException('Validation failed');
75+
throw new ValidationError('Validation failed');
7676
}
7777

7878
return bodyOrQuery;

packages/apps/fortune/recording-oracle/src/common/pipes/validation.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import {
2-
BadRequestException,
32
Injectable,
4-
ValidationError,
3+
ValidationError as ValidError,
54
ValidationPipe,
65
ValidationPipeOptions,
76
} from '@nestjs/common';
7+
import { ValidationError } from '../errors';
88

99
@Injectable()
1010
export class HttpValidationPipe extends ValidationPipe {
1111
constructor(options?: ValidationPipeOptions) {
1212
super({
13-
exceptionFactory: (errors: ValidationError[]): BadRequestException =>
14-
new BadRequestException(errors),
13+
exceptionFactory: (errors: ValidError[]): ValidationError => {
14+
const flattenErrors = this.flattenValidationErrors(errors);
15+
throw new ValidationError(flattenErrors.join(', '));
16+
},
1517
transform: true,
1618
whitelist: true,
1719
forbidNonWhitelisted: true,

0 commit comments

Comments
 (0)