From f6612e5a85a5c86aa3148018855a037c2d57e231 Mon Sep 17 00:00:00 2001 From: neo773 <62795688+neo773@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:13:50 +0530 Subject: [PATCH] fix(server): stop treating Microsoft Graph 401 as a permanent failure (#22989) In production we are seeing some accounts being occasionally marked as permanent failure even though when you check with their refresh token it never actually failed this PR stops treating 401 as permanent failure and treats them as Transient error. We already have token refresh stage that runs before the actual import stage, so if it's an actual revoke token error, it should catch it. Review in cubic --- ...arse-microsoft-calendar-error.util.spec.ts | 88 +++++++++++++ .../parse-microsoft-calendar-error.util.ts | 13 +- ...rse-microsoft-messages-import.util.spec.ts | 118 ++++++++++++++++++ .../parse-microsoft-messages-import.util.ts | 15 ++- 4 files changed, 214 insertions(+), 20 deletions(-) create mode 100644 packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/parse-microsoft-calendar-error.util.spec.ts create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/__tests__/parse-microsoft-messages-import.util.spec.ts diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/parse-microsoft-calendar-error.util.spec.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/parse-microsoft-calendar-error.util.spec.ts new file mode 100644 index 0000000000..8ebecd9f19 --- /dev/null +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/__tests__/parse-microsoft-calendar-error.util.spec.ts @@ -0,0 +1,88 @@ +import { type GraphError } from '@microsoft/microsoft-graph-client'; + +import { CalendarEventImportDriverExceptionCode } from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception'; +import { parseMicrosoftCalendarError } from 'src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util'; + +const buildGraphError = ({ + statusCode, + code = null, + message = 'error message', +}: { + statusCode: number; + code?: string | null; + message?: string; +}) => ({ statusCode, code, message }) as GraphError; + +describe('parseMicrosoftCalendarError', () => { + it('should be temporary when the access token is expired so the next attempt can refresh it', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ + statusCode: 401, + code: 'InvalidAuthenticationToken', + message: 'Lifetime validation failed, the token is expired.', + }), + ); + + expect(exception.code).toBe( + CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should be insufficient permissions when access is denied', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ statusCode: 403, code: 'ErrorAccessDenied' }), + ); + + expect(exception.code).toBe( + CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + }); + + it('should be not found for a 404 that is not a mailbox error', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ statusCode: 404, code: 'ResourceNotFound' }), + ); + + expect(exception.code).toBe( + CalendarEventImportDriverExceptionCode.NOT_FOUND, + ); + }); + + it('should be a sync cursor error when the delta token is no longer valid', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ statusCode: 410, code: 'SyncStateNotFound' }), + ); + + expect(exception.code).toBe( + CalendarEventImportDriverExceptionCode.SYNC_CURSOR_ERROR, + ); + }); + + it('should be temporary when throttled', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ statusCode: 429, code: 'TooManyRequests' }), + ); + + expect(exception.code).toBe( + CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should be temporary when the service is unavailable', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ statusCode: 503 }), + ); + + expect(exception.code).toBe( + CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should be unknown for an unhandled status code', () => { + const exception = parseMicrosoftCalendarError( + buildGraphError({ statusCode: 418 }), + ); + + expect(exception.code).toBe(CalendarEventImportDriverExceptionCode.UNKNOWN); + }); +}); diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util.ts index 153b7a1676..e373fa6895 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/drivers/microsoft-calendar/utils/parse-microsoft-calendar-error.util.ts @@ -26,17 +26,6 @@ export const parseMicrosoftCalendarError = ( ); case 404: - if ( - message?.includes( - 'The mailbox is either inactive, soft-deleted, or is hosted on-premise.', - ) - ) { - return new CalendarEventImportDriverException( - message, - CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, - ); - } - return new CalendarEventImportDriverException( message, CalendarEventImportDriverExceptionCode.NOT_FOUND, @@ -68,7 +57,7 @@ export const parseMicrosoftCalendarError = ( case 401: return new CalendarEventImportDriverException( message, - CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR, ); default: return new CalendarEventImportDriverException( diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/__tests__/parse-microsoft-messages-import.util.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/__tests__/parse-microsoft-messages-import.util.spec.ts new file mode 100644 index 0000000000..2c334d0243 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/__tests__/parse-microsoft-messages-import.util.spec.ts @@ -0,0 +1,118 @@ +import { MessageImportDriverExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; +import { parseMicrosoftMessagesImportError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/parse-microsoft-messages-import.util'; + +describe('parseMicrosoftMessagesImportError', () => { + it('should be temporary when the access token is expired so the next attempt can refresh it', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 401, + code: 'InvalidAuthenticationToken', + message: 'Lifetime validation failed, the token is expired.', + }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should keep the Microsoft error code and message so the failure stays diagnosable', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 401, + code: 'InvalidAuthenticationToken', + message: 'Lifetime validation failed, the token is expired.', + }); + + expect(exception.message).toContain('InvalidAuthenticationToken'); + expect(exception.message).toContain( + 'Lifetime validation failed, the token is expired.', + ); + }); + + it('should be insufficient permissions when access is denied', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 403, + code: 'ErrorAccessDenied', + message: 'Access is denied.', + }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + expect(exception.message).toContain('ErrorAccessDenied'); + }); + + it('should be insufficient permissions when the mailbox is not enabled for the REST API', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 404, + code: 'MailboxNotEnabledForRESTAPI', + message: + 'The mailbox is either inactive, soft-deleted, or is hosted on-premise.', + }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + }); + + it('should classify the mailbox error on its code rather than its message wording', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 404, + code: 'MailboxNotEnabledForRESTAPI', + message: 'Some reworded message from Microsoft.', + }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + ); + }); + + it('should be not found for a 404 that is not a mailbox error', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 404, + code: 'ResourceNotFound', + message: 'Resource not found.', + }); + + expect(exception.code).toBe(MessageImportDriverExceptionCode.NOT_FOUND); + }); + + it('should be a sync cursor error when the delta token is no longer valid', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 410, + code: 'SyncStateNotFound', + message: 'The sync state is not found.', + }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR, + ); + }); + + it('should be temporary when throttled', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 429, + code: 'TooManyRequests', + message: 'Please retry again later.', + }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should be temporary when a 400 has an empty error body', () => { + const exception = parseMicrosoftMessagesImportError({ statusCode: 400 }); + + expect(exception.code).toBe( + MessageImportDriverExceptionCode.TEMPORARY_ERROR, + ); + }); + + it('should be unknown for an unhandled status code', () => { + const exception = parseMicrosoftMessagesImportError({ + statusCode: 418, + message: 'I am a teapot.', + }); + + expect(exception.code).toBe(MessageImportDriverExceptionCode.UNKNOWN); + }); +}); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/parse-microsoft-messages-import.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/parse-microsoft-messages-import.util.ts index b207c0af43..3587c6f792 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/parse-microsoft-messages-import.util.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/microsoft/utils/parse-microsoft-messages-import.util.ts @@ -4,6 +4,9 @@ import { } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; import { isDefined } from 'twenty-shared/utils'; +const MICROSOFT_MAILBOX_NOT_ENABLED_FOR_REST_API_ERROR_CODE = + 'MailboxNotEnabledForRESTAPI'; + export const parseMicrosoftMessagesImportError = ( error: { statusCode: number; @@ -30,26 +33,22 @@ export const parseMicrosoftMessagesImportError = ( if (error.statusCode === 401) { return new MessageImportDriverException( - 'Unauthorized access to Microsoft Graph API', - MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + `Unauthorized access to Microsoft Graph API - code:${error.code} ${error.message}`, + MessageImportDriverExceptionCode.TEMPORARY_ERROR, { cause: options?.cause }, ); } if (error.statusCode === 403) { return new MessageImportDriverException( - 'Forbidden access to Microsoft Graph API', + `Forbidden access to Microsoft Graph API - code:${error.code} ${error.message}`, MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, { cause: options?.cause }, ); } if (error.statusCode === 404) { - if ( - error.message?.includes( - 'The mailbox is either inactive, soft-deleted, or is hosted on-premise.', - ) - ) { + if (error.code === MICROSOFT_MAILBOX_NOT_ENABLED_FOR_REST_API_ERROR_CODE) { return new MessageImportDriverException( `Disabled, deleted, inactive or no licence Microsoft account - code:${error.code}`, MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,