Improve error handling (#13130)

In the BE we throw custom errors with precise error codes (e.g.
"LABEL_ALREADY_EXISTS") before catching them in filters and rethrowing
BaseGraphQLErrors (standard errors such as NotFoundError, UserInputError
etc.).
In the FE we were grouping sentries based on the error codes but we were
actually grouping by very broad codes such as "NOT_FOUND" or
"BAD_USER_INPUT", extracted from the BaseGraphQLErrors.

To fix that, we update the BaseGraphQLError constructor api to allow to
pass on the CustomError directly and retrieve from it the original code
and store it in existing property `subCode` that we will use in the FE
to send errors to sentry.
This new api also eases usage of `userFriendlyMessage` that is passed on
to the api response and therefore to the FE when CustomError is passed
on directly to the BaseGraphQLError constructor.
This commit is contained in:
Marie
2025-07-09 17:13:44 +02:00
committed by GitHub
parent 484c267aa6
commit c13bc60dad
22 changed files with 319 additions and 123 deletions

View File

@ -18,21 +18,13 @@ export const fieldMetadataGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof FieldMetadataException) {
switch (error.code) {
case FieldMetadataExceptionCode.FIELD_METADATA_NOT_FOUND:
throw new NotFoundError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
throw new NotFoundError(error);
case FieldMetadataExceptionCode.INVALID_FIELD_INPUT:
throw new UserInputError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
throw new UserInputError(error);
case FieldMetadataExceptionCode.FIELD_MUTATION_NOT_ALLOWED:
throw new ForbiddenError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
throw new ForbiddenError(error);
case FieldMetadataExceptionCode.FIELD_ALREADY_EXISTS:
throw new ConflictError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
throw new ConflictError(error);
case FieldMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND:
case FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR:
case FieldMetadataExceptionCode.FIELD_METADATA_RELATION_NOT_ENABLED:

View File

@ -12,23 +12,19 @@ import { InvalidMetadataException } from 'src/engine/metadata-modules/utils/exce
export const objectMetadataGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof InvalidMetadataException) {
throw new UserInputError(error.message);
throw new UserInputError(error);
}
if (error instanceof ObjectMetadataException) {
switch (error.code) {
case ObjectMetadataExceptionCode.OBJECT_METADATA_NOT_FOUND:
throw new NotFoundError(error.message);
throw new NotFoundError(error);
case ObjectMetadataExceptionCode.INVALID_OBJECT_INPUT:
throw new UserInputError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
throw new UserInputError(error);
case ObjectMetadataExceptionCode.OBJECT_MUTATION_NOT_ALLOWED:
throw new ForbiddenError(error.message);
throw new ForbiddenError(error);
case ObjectMetadataExceptionCode.OBJECT_ALREADY_EXISTS:
throw new ConflictError(error.message, {
userFriendlyMessage: error.userFriendlyMessage,
});
throw new ConflictError(error);
case ObjectMetadataExceptionCode.MISSING_CUSTOM_OBJECT_DEFAULT_LABEL_IDENTIFIER_FIELD:
throw error;
default: {

View File

@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception';
export class PermissionsException extends CustomException {
declare code: PermissionsExceptionCode;
constructor(message: string, code: PermissionsExceptionCode) {
super(message, code);
constructor(
message: string,
code: PermissionsExceptionCode,
userFriendlyMessage?: string,
) {
super(message, code, userFriendlyMessage);
}
}

View File

@ -1,5 +1,3 @@
import { t } from '@lingui/core/macro';
import {
ForbiddenError,
NotFoundError,
@ -17,18 +15,17 @@ export const permissionGraphqlApiExceptionHandler = (
case PermissionsExceptionCode.PERMISSION_DENIED:
throw new ForbiddenError(error.message, {
userFriendlyMessage: 'User does not have permission.',
subCode: error.code,
});
case PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS:
throw new ForbiddenError(error.message, {
userFriendlyMessage: t`A role with this label already exists.`,
});
throw new ForbiddenError(error);
case PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN:
case PermissionsExceptionCode.CANNOT_UPDATE_SELF_ROLE:
case PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER:
case PermissionsExceptionCode.ROLE_NOT_EDITABLE:
case PermissionsExceptionCode.CANNOT_ADD_OBJECT_PERMISSION_ON_SYSTEM_OBJECT:
case PermissionsExceptionCode.CANNOT_ADD_FIELD_PERMISSION_ON_SYSTEM_OBJECT:
throw new ForbiddenError(error.message);
throw new ForbiddenError(error);
case PermissionsExceptionCode.INVALID_ARG:
case PermissionsExceptionCode.INVALID_SETTING:
case PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_ON_NON_READABLE_OBJECT:
@ -37,13 +34,13 @@ export const permissionGraphqlApiExceptionHandler = (
case PermissionsExceptionCode.FIELD_RESTRICTION_ONLY_ALLOWED_ON_READABLE_OBJECT:
case PermissionsExceptionCode.FIELD_RESTRICTION_ON_UPDATE_ONLY_ALLOWED_ON_UPDATABLE_OBJECT:
case PermissionsExceptionCode.EMPTY_FIELD_PERMISSION_NOT_ALLOWED:
throw new UserInputError(error.message);
throw new UserInputError(error);
case PermissionsExceptionCode.ROLE_NOT_FOUND:
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
case PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND:
case PermissionsExceptionCode.FIELD_METADATA_NOT_FOUND:
case PermissionsExceptionCode.PERMISSION_NOT_FOUND:
throw new NotFoundError(error.message);
throw new NotFoundError(error);
case PermissionsExceptionCode.UPSERT_FIELD_PERMISSION_FAILED:
case PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND:
case PermissionsExceptionCode.WORKSPACE_ID_ROLE_USER_WORKSPACE_MISMATCH:

View File

@ -1,5 +1,6 @@
import { InjectRepository } from '@nestjs/typeorm';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
@ -286,6 +287,7 @@ export class RoleService {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_LABEL_ALREADY_EXISTS,
PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS,
t`A role with this label already exists.`,
);
}
}

View File

@ -14,13 +14,13 @@ export const serverlessFunctionGraphQLApiExceptionHandler = (error: any) => {
switch (error.code) {
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND:
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_VERSION_NOT_FOUND:
throw new NotFoundError(error.message);
throw new NotFoundError(error);
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_ALREADY_EXIST:
throw new ConflictError(error.message);
throw new ConflictError(error);
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_READY:
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_BUILDING:
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED:
throw new ForbiddenError(error.message);
throw new ForbiddenError(error);
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CODE_UNCHANGED:
throw error;
default: {