Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 03db2b1837 fix: handle network errors as temporary in Microsoft calendar import
https://sonarly.com/issue/19081?type=bug

Transient network failures (TCP-level "fetch failed") when calling Microsoft Graph API during calendar event import are misclassified as UNKNOWN errors, causing the calendar channel to be permanently marked as failed instead of being retried.

Fix: The root cause is that `parseMicrosoftCalendarError` only switches on HTTP `statusCode`. When Node.js undici throws `TypeError: fetch failed` (a TCP-level network failure), there is no `statusCode` — the error falls to `default` and is classified as UNKNOWN. This triggers `handleUnknownException`, which permanently marks the calendar channel as FAILED_UNKNOWN and flushes all pending events — an unrecoverable state for a transient network blip.

Changes made:

1. **parse-microsoft-calendar-error.util.ts** (primary fix):
   - Added `isNetworkError()` helper that detects TypeError instances (undici fetch failures) and errors with `cause.code` matching known network error codes (ECONNRESET, ENOTFOUND, ETIMEDOUT, ECONNABORTED, ECONNREFUSED, EHOSTUNREACH, ERR_NETWORK, UND_ERR_CONNECT_TIMEOUT, UND_ERR_SOCKET).
   - Network errors are now classified as TEMPORARY_ERROR → retry with backoff instead of permanent failure.
   - Added safety-net guard: errors with no `statusCode` that aren't detected as network errors also get TEMPORARY_ERROR (fail-safe toward retry).
   - Reclassified HTTP 500/502/503/504 from UNKNOWN to TEMPORARY_ERROR, matching the messaging-side pattern.

2. **parse-microsoft-messages-import.util.ts** (proactive fix):
   - Added the same `isNetworkError()` guard before HTTP status code checks, preventing the identical bug from manifesting in Microsoft message import.
2026-03-27 17:07:26 +00:00
2 changed files with 86 additions and 5 deletions
@@ -4,12 +4,53 @@ import {
CalendarEventImportDriverException,
CalendarEventImportDriverExceptionCode,
} from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception';
import { isDefined } from 'twenty-shared/utils';
const isNetworkError = (error: GraphError): boolean => {
if (error instanceof TypeError) {
return true;
}
const cause = (error as unknown as { cause?: { code?: string } })?.cause;
if (!isDefined(cause)) {
return false;
}
const networkErrorCodes = [
'ECONNRESET',
'ENOTFOUND',
'ECONNABORTED',
'ETIMEDOUT',
'ECONNREFUSED',
'EHOSTUNREACH',
'ERR_NETWORK',
'UND_ERR_CONNECT_TIMEOUT',
'UND_ERR_SOCKET',
];
return isDefined(cause.code) && networkErrorCodes.includes(cause.code);
};
export const parseMicrosoftCalendarError = (
error: GraphError,
): CalendarEventImportDriverException => {
if (isNetworkError(error)) {
return new CalendarEventImportDriverException(
`Microsoft Calendar network error: ${error.message}`,
CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR,
);
}
const { statusCode, message } = error;
if (!isDefined(statusCode)) {
return new CalendarEventImportDriverException(
`Microsoft Calendar error without status code: ${message}`,
CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR,
);
}
switch (statusCode) {
case 400:
return new CalendarEventImportDriverException(
@@ -40,6 +81,10 @@ export const parseMicrosoftCalendarError = (
);
case 429:
case 500:
case 502:
case 503:
case 504:
return new CalendarEventImportDriverException(
message,
CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR,
@@ -56,11 +101,6 @@ export const parseMicrosoftCalendarError = (
message,
CalendarEventImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
case 500:
return new CalendarEventImportDriverException(
message,
CalendarEventImportDriverExceptionCode.UNKNOWN,
);
default:
return new CalendarEventImportDriverException(
@@ -4,6 +4,39 @@ import {
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { isDefined } from 'twenty-shared/utils';
const isNetworkError = (error: {
statusCode: number;
message?: string;
}): boolean => {
if (error instanceof TypeError) {
return true;
}
if (isDefined(error.statusCode)) {
return false;
}
const cause = (error as unknown as { cause?: { code?: string } })?.cause;
if (!isDefined(cause) || !isDefined(cause.code)) {
return false;
}
const networkErrorCodes = [
'ECONNRESET',
'ENOTFOUND',
'ECONNABORTED',
'ETIMEDOUT',
'ECONNREFUSED',
'EHOSTUNREACH',
'ERR_NETWORK',
'UND_ERR_CONNECT_TIMEOUT',
'UND_ERR_SOCKET',
];
return networkErrorCodes.includes(cause.code);
};
export const parseMicrosoftMessagesImportError = (
error: {
statusCode: number;
@@ -12,6 +45,14 @@ export const parseMicrosoftMessagesImportError = (
},
options?: { cause?: Error },
): MessageImportDriverException => {
if (isNetworkError(error)) {
return new MessageImportDriverException(
`Microsoft Graph API network error: ${error.message}`,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
}
if (error.statusCode === 400) {
if (!isDefined(error.message)) {
return new MessageImportDriverException(