From aa519e7d81343e09c75fa72d1a33889113f25a60 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 9 Dec 2025 17:01:25 +0100 Subject: [PATCH] Fix messaging errors on message folder list (#16404) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a known quirk of many Google APIs, including Gmail. Some error responses use numeric code fields (e.g., 403), while others may return them as strings (e.g., "403"). This depends on which internal service returns the error and the language client you’re using. --- .../gmail/mocks/gmail-api-error-mocks.ts | 177 +++++++++----- .../gmail/types/gmail-api-error.type.ts | 2 +- ...rse-gmail-message-list-fetch-error.spec.ts | 228 +++++++++++++++++- .../utils/is-gmail-api-error-error.util.ts | 2 +- .../gmail/utils/parse-gmail-api-error.util.ts | 22 +- 5 files changed, 347 insertions(+), 84 deletions(-) diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks.ts index 6f2efa7bfc..7f7570b531 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks.ts @@ -3,112 +3,171 @@ import { type GmailApiError } from 'src/modules/messaging/message-import-manager const gmailApiErrorMocks = { // 400 Bad Request - Invalid query parameters badRequest: { - code: '400', + code: 400, message: 'badRequest', }, // 400 Invalid Grant invalidGrant: { - code: '400', + code: 400, message: 'invalid_grant', }, // 400 Failed Precondition failedPrecondition: { - code: '400', + code: 400, message: 'failedPrecondition', }, invalidCredentials: { - code: '401', + code: 401, message: 'authError', }, notFound: { - code: '404', + code: 404, message: 'notFound', }, gone: { - code: '410', + code: 410, message: 'resourceGone', }, dailyLimitExceeded: { - code: '403', + code: 403, message: 'dailyLimitExceeded', }, userRateLimitExceeded: { - code: '403', + code: 403, message: 'userRateLimitExceeded', }, rateLimitExceeded: { - code: '403', + code: 403, message: 'rateLimitExceeded', }, domainPolicyError: { - code: '403', + code: 403, message: 'domainPolicy', }, tooManyConcurrentRequests: { - code: '429', + code: 429, message: 'tooManyConcurrentRequests', }, backendError: { - code: '500', + code: 500, message: 'backendError', }, - - getError: function (code: number, type?: string): GmailApiError { - switch (code) { - case 400: - switch (type) { - case 'invalid_grant': - return this.invalidGrant; - case 'failedPrecondition': - return this.failedPrecondition; - default: - return this.badRequest; - } - case 401: - return this.invalidCredentials; - case 403: - switch (type) { - case 'dailyLimit': - return this.dailyLimitExceeded; - case 'userRateLimit': - return this.userRateLimitExceeded; - case 'rateLimit': - return this.rateLimitExceeded; - case 'domainPolicy': - return this.domainPolicyError; - default: - return this.rateLimitExceeded; - } - case 404: - return this.notFound; - case 410: - return this.gone; - case 429: - switch (type) { - case 'concurrent': - return this.tooManyConcurrentRequests; - case 'mailSending': - return this.mailSendingLimitExceeded; - default: - return this.tooManyConcurrentRequests; - } - case 500: - return this.backendError; - default: - throw new Error(`Unknown error code: ${code}`); - } - }, }; -export default gmailApiErrorMocks; +const convertToErrorWithErrorCodeStringOrNumber = ({ + error, + errorCodeAsString, +}: { + error: GmailApiError; + errorCodeAsString: boolean; +}): GmailApiError => { + return { + code: errorCodeAsString ? error.code.toString() : error.code, + message: error.message, + }; +}; + +export const getGmailApiError = ({ + code, + type, + errorCodeAsString = false, +}: { + code: number; + type?: string; + errorCodeAsString?: boolean; +}): GmailApiError => { + switch (code) { + case 400: + switch (type) { + case 'invalid_grant': + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.invalidGrant, + errorCodeAsString, + }); + case 'failedPrecondition': + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.failedPrecondition, + errorCodeAsString, + }); + default: + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.badRequest, + errorCodeAsString, + }); + } + case 401: + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.invalidCredentials, + errorCodeAsString, + }); + case 403: + switch (type) { + case 'dailyLimit': + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.dailyLimitExceeded, + errorCodeAsString, + }); + case 'userRateLimit': + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.userRateLimitExceeded, + errorCodeAsString, + }); + case 'rateLimit': + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.rateLimitExceeded, + errorCodeAsString, + }); + case 'domainPolicy': + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.domainPolicyError, + errorCodeAsString, + }); + default: + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.rateLimitExceeded, + errorCodeAsString, + }); + } + case 404: + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.notFound, + errorCodeAsString, + }); + case 410: + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.gone, + errorCodeAsString, + }); + case 429: + switch (type) { + case 'concurrent': + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.tooManyConcurrentRequests, + errorCodeAsString, + }); + default: + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.tooManyConcurrentRequests, + errorCodeAsString, + }); + } + case 500: + return convertToErrorWithErrorCodeStringOrNumber({ + error: gmailApiErrorMocks.backendError, + errorCodeAsString, + }); + default: + throw new Error(`Unknown error code: ${code}`); + } +}; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-error.type.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-error.type.ts index efe35f0757..22132d2cc7 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-error.type.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-error.type.ts @@ -1,4 +1,4 @@ export type GmailApiError = { - code: string; + code: number | string; message: string; }; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-message-list-fetch-error.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-message-list-fetch-error.spec.ts index 08d16fffb4..5706e25365 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-message-list-fetch-error.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-message-list-fetch-error.spec.ts @@ -2,12 +2,15 @@ import { MessageImportDriverException, MessageImportDriverExceptionCode, } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; -import gmailApiErrorMocks from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks'; +import { getGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks'; import { parseGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util'; describe('parseGmailApiError', () => { it('should handle 400 Bad Request', () => { - const error = gmailApiErrorMocks.getError(400); + const error = getGmailApiError({ + code: 400, + errorCodeAsString: false, + }); const exception = parseGmailApiError(error); expect(exception).toBeInstanceOf(MessageImportDriverException); @@ -15,7 +18,11 @@ describe('parseGmailApiError', () => { }); it('should handle 400 Invalid Grant', () => { - const error = gmailApiErrorMocks.getError(400, 'invalid_grant'); + const error = getGmailApiError({ + code: 400, + type: 'invalid_grant', + errorCodeAsString: false, + }); const exception = parseGmailApiError(error); expect(exception).toBeInstanceOf(MessageImportDriverException); @@ -25,7 +32,11 @@ describe('parseGmailApiError', () => { }); it('should handle 400 Failed Precondition', () => { - const error = gmailApiErrorMocks.getError(400, 'failedPrecondition'); + const error = getGmailApiError({ + code: 400, + type: 'failedPrecondition', + errorCodeAsString: false, + }); const exception = parseGmailApiError(error); expect(exception).toBeInstanceOf(MessageImportDriverException); @@ -35,7 +46,10 @@ describe('parseGmailApiError', () => { }); it('should handle 401 Invalid Credentials', () => { - const error = gmailApiErrorMocks.getError(401); + const error = getGmailApiError({ + code: 401, + errorCodeAsString: false, + }); const exception = parseGmailApiError(error); expect(exception).toBeInstanceOf(MessageImportDriverException); @@ -45,7 +59,11 @@ describe('parseGmailApiError', () => { }); it('should handle 403 Daily Limit Exceeded', () => { - const error = gmailApiErrorMocks.getError(403, 'dailyLimit'); + const error = getGmailApiError({ + code: 403, + type: 'dailyLimit', + errorCodeAsString: false, + }); const exception = parseGmailApiError(error); expect(exception).toBeInstanceOf(MessageImportDriverException); @@ -55,7 +73,11 @@ describe('parseGmailApiError', () => { }); it('should handle 403 User Rate Limit Exceeded', () => { - const error = gmailApiErrorMocks.getError(403, 'userRateLimit'); + const error = getGmailApiError({ + code: 403, + type: 'userRateLimit', + errorCodeAsString: false, + }); const exception = parseGmailApiError(error); expect(exception).toBeInstanceOf(MessageImportDriverException); @@ -65,7 +87,11 @@ describe('parseGmailApiError', () => { }); it('should handle 403 Rate Limit Exceeded', () => { - const error = gmailApiErrorMocks.getError(403, 'rateLimit'); + const error = getGmailApiError({ + code: 403, + type: 'rateLimit', + errorCodeAsString: false, + }); const exception = parseGmailApiError(error); expect(exception).toBeInstanceOf(MessageImportDriverException); @@ -75,7 +101,11 @@ describe('parseGmailApiError', () => { }); it('should handle 403 Domain Policy Error', () => { - const error = gmailApiErrorMocks.getError(403, 'domainPolicy'); + const error = getGmailApiError({ + code: 403, + type: 'domainPolicy', + errorCodeAsString: false, + }); const exception = parseGmailApiError(error); expect(exception).toBeInstanceOf(MessageImportDriverException); @@ -85,7 +115,10 @@ describe('parseGmailApiError', () => { }); it('should handle 404 as sync cursor error', () => { - const error = gmailApiErrorMocks.getError(404); + const error = getGmailApiError({ + code: 404, + errorCodeAsString: false, + }); const exception = parseGmailApiError(error); expect(exception).toBeInstanceOf(MessageImportDriverException); @@ -95,7 +128,10 @@ describe('parseGmailApiError', () => { }); it('should handle 410 Gone', () => { - const error = gmailApiErrorMocks.getError(410); + const error = getGmailApiError({ + code: 410, + errorCodeAsString: false, + }); const exception = parseGmailApiError(error); expect(exception).toBeInstanceOf(MessageImportDriverException); @@ -103,7 +139,11 @@ describe('parseGmailApiError', () => { }); it('should handle 429 Too Many Requests', () => { - const error = gmailApiErrorMocks.getError(429, 'concurrent'); + const error = getGmailApiError({ + code: 429, + type: 'concurrent', + errorCodeAsString: false, + }); const exception = parseGmailApiError(error); expect(exception).toBeInstanceOf(MessageImportDriverException); @@ -113,7 +153,169 @@ describe('parseGmailApiError', () => { }); it('should handle 500 Backend Error', () => { - const error = gmailApiErrorMocks.getError(500); + const error = getGmailApiError({ + code: 500, + errorCodeAsString: false, + }); + const exception = parseGmailApiError(error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should handle 400 Bad Request with errorCodeAsString', () => { + const error = getGmailApiError({ + code: 400, + errorCodeAsString: true, + }); + const exception = parseGmailApiError(error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe(MessageImportDriverExceptionCode.UNKNOWN); + }); + + it('should handle 400 Invalid Grant with errorCodeAsString', () => { + const error = getGmailApiError({ + code: 400, + type: 'invalid_grant', + errorCodeAsString: true, + }); + const exception = parseGmailApiError(error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + }); + + it('should handle 400 Failed Precondition with errorCodeAsString', () => { + const error = getGmailApiError({ + code: 400, + type: 'failedPrecondition', + errorCodeAsString: true, + }); + const exception = parseGmailApiError(error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should handle 401 Invalid Credentials with errorCodeAsString', () => { + const error = getGmailApiError({ + code: 401, + errorCodeAsString: true, + }); + const exception = parseGmailApiError(error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + }); + + it('should handle 403 Daily Limit Exceeded with errorCodeAsString', () => { + const error = getGmailApiError({ + code: 403, + type: 'dailyLimit', + errorCodeAsString: true, + }); + const exception = parseGmailApiError(error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should handle 403 User Rate Limit Exceeded with errorCodeAsString', () => { + const error = getGmailApiError({ + code: 403, + type: 'userRateLimit', + errorCodeAsString: true, + }); + const exception = parseGmailApiError(error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should handle 403 Rate Limit Exceeded with errorCodeAsString', () => { + const error = getGmailApiError({ + code: 403, + type: 'rateLimit', + errorCodeAsString: true, + }); + const exception = parseGmailApiError(error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should handle 403 Domain Policy Error with errorCodeAsString', () => { + const error = getGmailApiError({ + code: 403, + type: 'domainPolicy', + errorCodeAsString: true, + }); + const exception = parseGmailApiError(error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + }); + + it('should handle 404 as sync cursor error with errorCodeAsString', () => { + const error = getGmailApiError({ + code: 404, + errorCodeAsString: true, + }); + const exception = parseGmailApiError(error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR, + ); + }); + + it('should handle 410 Gone with errorCodeAsString', () => { + const error = getGmailApiError({ + code: 410, + errorCodeAsString: true, + }); + const exception = parseGmailApiError(error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe(MessageImportDriverExceptionCode.UNKNOWN); + }); + + it('should handle 429 Too Many Requests with errorCodeAsString', () => { + const error = getGmailApiError({ + code: 429, + type: 'concurrent', + errorCodeAsString: true, + }); + const exception = parseGmailApiError(error); + + expect(exception).toBeInstanceOf(MessageImportDriverException); + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should handle 500 Backend Error with errorCodeAsString', () => { + const error = getGmailApiError({ + code: 500, + errorCodeAsString: true, + }); const exception = parseGmailApiError(error); expect(exception).toBeInstanceOf(MessageImportDriverException); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-error-error.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-error-error.util.ts index 5e675532f3..48db413567 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-error-error.util.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-error-error.util.ts @@ -7,7 +7,7 @@ export const isGmailApiError = (error: unknown): error is GmailApiError => { if ( !('code' in error) || - typeof error.code !== 'string' || + (typeof error.code !== 'number' && typeof error.code !== 'string') || !('message' in error) || typeof error.message !== 'string' ) { diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util.ts index 6721e39423..a424869de7 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util.ts @@ -9,8 +9,10 @@ export const parseGmailApiError = ( ): MessageImportDriverException => { const { code, message } = error; - switch (code) { - case '400': + const codeAsNumber = Number(code); + + switch (codeAsNumber) { + case 400: if (message === 'invalid_grant') { return new MessageImportDriverException( message, @@ -36,19 +38,19 @@ export const parseGmailApiError = ( MessageImportDriverExceptionCode.UNKNOWN, ); - case '404': + case 404: return new MessageImportDriverException( message, MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR, ); - case '429': + case 429: return new MessageImportDriverException( message, MessageImportDriverExceptionCode.TEMPORARY_ERROR, ); - case '403': + case 403: if ( message === 'rateLimitExceeded' || message === 'userRateLimitExceeded' || @@ -68,21 +70,21 @@ export const parseGmailApiError = ( break; - case '401': + case 401: return new MessageImportDriverException( message, MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, ); - case '503': + case 503: return new MessageImportDriverException( message, MessageImportDriverExceptionCode.TEMPORARY_ERROR, ); - case '500': - case '502': - case '504': + case 500: + case 502: + case 504: if (message === 'backendError') { return new MessageImportDriverException( message,