From 806bb611e8332355c38610eb06c54f71bc60d3d7 Mon Sep 17 00:00:00 2001 From: Weiko Date: Fri, 16 May 2025 18:11:52 +0200 Subject: [PATCH] Fix yoga scalar validations being captured (#12085) Yoga graphql error were not correctly interpreted by the exception handler. Mostly validations on the scalars such as bad enum options, wrong format for uuid and such. This PR adds a new convertGraphQLErrorToBaseGraphQLError utility function in graphql-errors.util.ts that converts those errors to our custom BaseGraphQLError by using the extension.http.code from the error when possible so they can be handled the same way we treat the graphql errors we throw ourselves. Before Screenshot 2025-05-16 at 11 04 08 After Screenshot 2025-05-16 at 11 16 37 --- .../hooks/use-graphql-error-handler.hook.ts | 35 ++++++++++---- .../graphql/utils/graphql-errors.util.ts | 48 +++++++++++++++++++ 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook.ts b/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook.ts index 3f181f418..4bdc9860d 100644 --- a/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook.ts +++ b/packages/twenty-server/src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook.ts @@ -10,7 +10,10 @@ import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/interfaces import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { generateGraphQLErrorFromError } from 'src/engine/core-modules/graphql/utils/generate-graphql-error-from-error.util'; -import { BaseGraphQLError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; +import { + BaseGraphQLError, + convertGraphQLErrorToBaseGraphQLError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { shouldCaptureException } from 'src/engine/utils/global-exception-handler.util'; const DEFAULT_EVENT_ID_KEY = 'exceptionEventId'; @@ -81,13 +84,29 @@ export const useGraphQLErrorHandlerHook = < return; } - // Step 1: Flatten errors - extract original errors when available - const originalErrors = result.errors.map((error) => { - return error.originalError || error; + // Step 1: Process errors - extract original errors and convert to BaseGraphQLError + const processedErrors = result.errors.map((error) => { + const originalError = error.originalError || error; + + if (error.extensions && originalError !== error) { + originalError.extensions = { + ...error.extensions, + ...(originalError.extensions || {}), + }; + } + + if ( + originalError instanceof GraphQLError && + !(originalError instanceof BaseGraphQLError) + ) { + return convertGraphQLErrorToBaseGraphQLError(originalError); + } + + return originalError; }); // Step 2: Send errors to monitoring service (with stack traces) - const errorsToCapture = originalErrors.filter( + const errorsToCapture = processedErrors.filter( shouldCaptureException, ); @@ -107,15 +126,15 @@ export const useGraphQLErrorHandlerHook = < errorsToCapture.forEach((_, i) => { if (eventIds?.[i] && eventIdKey !== null) { - originalErrors[ - originalErrors.indexOf(errorsToCapture[i]) + processedErrors[ + processedErrors.indexOf(errorsToCapture[i]) ].eventId = eventIds[i]; } }); } // Step 3: Transform errors for GraphQL response (clean GraphQL errors) - const transformedErrors = originalErrors.map((error) => { + const transformedErrors = processedErrors.map((error) => { const graphqlError = error instanceof BaseGraphQLError ? error diff --git a/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts b/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts index 72c080ee5..8a4afa07c 100644 --- a/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts +++ b/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts @@ -196,3 +196,51 @@ export class InternalServerError extends BaseGraphQLError { Object.defineProperty(this, 'name', { value: 'InternalServerError' }); } } + +/** + * Converts a GraphQLError to a BaseGraphQLError with the appropriate ErrorCode + * based on HTTP status code if present in extensions. + */ +export const convertGraphQLErrorToBaseGraphQLError = ( + error: GraphQLError, +): BaseGraphQLError => { + const httpStatus = error.extensions?.http?.status; + let errorCode = ErrorCode.INTERNAL_SERVER_ERROR; + + if (httpStatus && typeof httpStatus === 'number') { + switch (httpStatus) { + case 400: + errorCode = ErrorCode.BAD_USER_INPUT; + break; + case 401: + errorCode = ErrorCode.UNAUTHENTICATED; + break; + case 403: + errorCode = ErrorCode.FORBIDDEN; + break; + case 404: + errorCode = ErrorCode.NOT_FOUND; + break; + case 405: + errorCode = ErrorCode.METHOD_NOT_ALLOWED; + break; + case 408: + case 504: + errorCode = ErrorCode.TIMEOUT; + break; + case 409: + errorCode = ErrorCode.CONFLICT; + break; + default: + if (httpStatus >= 400 && httpStatus < 500) { + // Other 4xx errors + errorCode = ErrorCode.BAD_USER_INPUT; + } else { + // 5xx errors default to internal server error + errorCode = ErrorCode.INTERNAL_SERVER_ERROR; + } + } + } + + return new BaseGraphQLError(error.message, errorCode, error.extensions); +};