Files
twenty_crm/packages/twenty-server/src/engine/utils/global-exception-handler.util.ts
Marie 1b72d901a5 Improve RestApiExceptionFilter (#12967)
RestApiExceptionFilter is used as an exception filter for the core
controller which is used for crud operations on our objects (equivalent
of our dynamic queries findManyPeople etc. on the graphql API).

Exceptions were leading a 400 / BadRequestException response status
which can be confusing to users.
By default we should actually throw a 500 if the error was not handled
priorily, but we have not implemented input validation for the REST api
so we fear to be flooded with errors that should not be 500 but 400 due
to user inputs. A solution should be brought [with this
ticket](https://github.com/twentyhq/core-team-issues/issues/1027) but it
has not been prioritized yet.
2025-06-30 16:01:48 +02:00

161 lines
4.1 KiB
TypeScript

import { HttpException } from '@nestjs/common';
import { GraphQLError } from 'graphql';
import { ExceptionHandlerUser } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-user.interface';
import { ExceptionHandlerWorkspace } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-workspace.interface';
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import {
AuthenticationError,
BaseGraphQLError,
ConflictError,
ErrorCode,
ForbiddenError,
MethodNotAllowedError,
NotFoundError,
TimeoutError,
ValidationError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { CustomException } from 'src/utils/custom-exception';
const graphQLPredefinedExceptions = {
400: ValidationError,
401: AuthenticationError,
403: ForbiddenError,
404: NotFoundError,
405: MethodNotAllowedError,
408: TimeoutError,
409: ConflictError,
};
export const graphQLErrorCodesToFilter = [
ErrorCode.GRAPHQL_VALIDATION_FAILED,
ErrorCode.UNAUTHENTICATED,
ErrorCode.FORBIDDEN,
ErrorCode.NOT_FOUND,
ErrorCode.METHOD_NOT_ALLOWED,
ErrorCode.TIMEOUT,
ErrorCode.CONFLICT,
ErrorCode.BAD_USER_INPUT,
];
export const handleExceptionAndConvertToGraphQLError = (
exception: Error,
exceptionHandlerService: ExceptionHandlerService,
user?: ExceptionHandlerUser,
workspace?: ExceptionHandlerWorkspace,
): BaseGraphQLError => {
handleException({
exception,
exceptionHandlerService,
user,
workspace,
});
return convertExceptionToGraphQLError(exception);
};
export const shouldCaptureException = (
exception: Error,
statusCode?: number,
): boolean => {
if (
exception instanceof GraphQLError &&
(exception?.extensions?.http?.status ?? 500) < 500
) {
return false;
}
if (
exception instanceof BaseGraphQLError &&
graphQLErrorCodesToFilter.includes(exception?.extensions?.code)
) {
return false;
}
if (exception instanceof HttpException && exception.getStatus() < 500) {
return false;
}
if (statusCode && statusCode < 500) {
return false;
}
return true;
};
export const handleException = <
T extends Error | CustomException | HttpException,
>({
exception,
exceptionHandlerService,
user,
workspace,
statusCode,
}: {
exception: T;
exceptionHandlerService: ExceptionHandlerService;
user?: ExceptionHandlerUser;
workspace?: ExceptionHandlerWorkspace;
statusCode?: number;
}): T => {
if (shouldCaptureException(exception, statusCode)) {
exceptionHandlerService.captureExceptions([exception], { user, workspace });
}
return exception;
};
export const convertExceptionToGraphQLError = (
exception: Error,
): BaseGraphQLError => {
if (exception instanceof HttpException) {
return convertHttpExceptionToGraphql(exception);
}
if (exception instanceof BaseGraphQLError) {
return exception;
}
return convertExceptionToGraphql(exception);
};
const convertHttpExceptionToGraphql = (exception: HttpException) => {
const status = exception.getStatus();
let error: BaseGraphQLError;
if (status in graphQLPredefinedExceptions) {
// @ts-expect-error legacy noImplicitAny
const message = exception.getResponse()['message'] ?? exception.message;
// @ts-expect-error legacy noImplicitAny
error = new graphQLPredefinedExceptions[exception.getStatus()](message);
} else {
error = new BaseGraphQLError(
'Internal Server Error',
exception.getStatus().toString(),
);
}
// Only show the stack trace in development mode
if (process.env.NODE_ENV === NodeEnvironment.DEVELOPMENT) {
error.stack = exception.stack;
error.extensions['response'] = exception.getResponse();
}
return error;
};
export const convertExceptionToGraphql = (exception: Error) => {
const error = new BaseGraphQLError(
exception.name,
ErrorCode.INTERNAL_SERVER_ERROR,
);
error.stack = exception.stack;
error.extensions['response'] = exception.message;
return error;
};