Refactor error messages messaging (#16094)

We should try catch locally gmail errors when:
- fetching message list
- fetching messages
- refreshing aliases
- fetching folders
This commit is contained in:
Charles Bochet
2025-11-26 18:03:44 +01:00
committed by GitHub
parent fc4dbb80de
commit 5202e2b2db
32 changed files with 543 additions and 399 deletions
@@ -0,0 +1,33 @@
import { Injectable, Logger } from '@nestjs/common';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { isGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-error-error.util';
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
import { parseGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util';
import { parseGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-network-error.util';
@Injectable()
export class GmailFoldersErrorHandlerService {
private readonly logger = new Logger(GmailFoldersErrorHandlerService.name);
constructor() {}
public handleError(error: unknown): void {
this.logger.error(`Gmail: Error fetching folders: ${error}`);
if (isGmailNetworkError(error)) {
throw parseGmailNetworkError(error);
}
if (isGmailApiError(error)) {
throw parseGmailApiError(error);
}
throw new MessageImportDriverException(
'Unknown error',
MessageImportDriverExceptionCode.UNKNOWN,
);
}
}
@@ -10,11 +10,11 @@ import {
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { GmailFoldersErrorHandlerService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-folders-error-handler.service';
import { extractGmailFolderName } from 'src/modules/messaging/message-folder-manager/drivers/gmail/utils/extract-gmail-folder-name.util';
import { getGmailFolderParentId } from 'src/modules/messaging/message-folder-manager/drivers/gmail/utils/get-gmail-folder-parent-id.util';
import { shouldSyncFolderByDefault } from 'src/modules/messaging/message-folder-manager/utils/should-sync-folder-by-default.util';
import { MESSAGING_GMAIL_DEFAULT_NOT_SYNCED_LABELS } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-default-not-synced-labels';
import { GmailMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-message-list-fetch-error-handler.service';
@Injectable()
export class GmailGetAllFoldersService implements MessageFolderDriver {
@@ -22,7 +22,7 @@ export class GmailGetAllFoldersService implements MessageFolderDriver {
constructor(
private readonly oAuth2ClientManagerService: OAuth2ClientManagerService,
private readonly gmailMessageListFetchErrorHandler: GmailMessageListFetchErrorHandler,
private readonly gmailFoldersErrorHandlerService: GmailFoldersErrorHandlerService,
) {}
async getAllMessageFolders(
@@ -53,7 +53,7 @@ export class GmailGetAllFoldersService implements MessageFolderDriver {
`Connected account ${connectedAccount.id}: Error fetching labels: ${error.message}`,
);
this.gmailMessageListFetchErrorHandler.handleError(error);
this.gmailFoldersErrorHandlerService.handleError(error);
return { data: { labels: [] } };
});
@@ -6,9 +6,10 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/imap-get-all-folders.service';
import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/microsoft-get-all-folders.service';
import { GmailFoldersErrorHandlerService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-folders-error-handler.service';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/services/imap-get-all-folders.service';
import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/services/microsoft-get-all-folders.service';
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module';
import { MessagingIMAPDriverModule } from 'src/modules/messaging/message-import-manager/drivers/imap/messaging-imap-driver.module';
@@ -28,6 +29,7 @@ import { MessagingMicrosoftDriverModule } from 'src/modules/messaging/message-im
providers: [
SyncMessageFoldersService,
GmailGetAllFoldersService,
GmailFoldersErrorHandlerService,
ImapGetAllFoldersService,
MicrosoftGetAllFoldersService,
],
@@ -14,9 +14,9 @@ import {
MessageFolderPendingSyncAction,
type MessageFolderWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/imap-get-all-folders.service';
import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/microsoft-get-all-folders.service';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/services/imap-get-all-folders.service';
import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/services/microsoft-get-all-folders.service';
import { MessageFolderName } from 'src/modules/messaging/message-import-manager/drivers/microsoft/types/folders';
type SyncMessageFoldersInput = {
@@ -17,7 +17,6 @@ import { GmailGetMessageListService } from 'src/modules/messaging/message-import
import { GmailGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service';
import { GmailMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-message-list-fetch-error-handler.service';
import { GmailMessagesImportErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-messages-import-error-handler.service';
import { GmailNetworkErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-network-error-handler.service';
import { MessageParticipantManagerModule } from 'src/modules/messaging/message-participant-manager/message-participant-manager.module';
@Module({
@@ -40,14 +39,12 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p
GmailFetchByBatchService,
GmailGetMessagesService,
GmailGetMessageListService,
GmailNetworkErrorHandler,
GmailMessageListFetchErrorHandler,
GmailMessagesImportErrorHandler,
],
exports: [
GmailGetMessagesService,
GmailGetMessageListService,
GmailNetworkErrorHandler,
GmailMessageListFetchErrorHandler,
GmailMessagesImportErrorHandler,
],
@@ -1,196 +1,70 @@
// Gmail API Error Response Mocks for users.messages.list
import { type GmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-error.type';
const gmailApiErrorMocks = {
// 400 Bad Request - Invalid query parameters
badRequest: {
error: {
code: 400,
errors: [
{
domain: 'global',
location: 'orderBy',
locationType: 'parameter',
message:
'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.',
reason: 'badRequest',
},
],
message:
'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.',
},
code: '400',
message: 'badRequest',
},
// 400 Invalid Grant
invalidGrant: {
error: {
code: 400,
errors: [
{
domain: 'global',
reason: 'invalid_grant',
message: 'Invalid Credentials',
},
],
message: 'Invalid Credentials',
},
code: '400',
message: 'invalid_grant',
},
// 400 Failed Precondition
failedPrecondition: {
error: {
code: 400,
errors: [
{
domain: 'global',
reason: 'failedPrecondition',
message: 'Failed Precondition',
},
],
message: 'Failed Precondition',
},
code: '400',
message: 'failedPrecondition',
},
// 401 Invalid Credentials
invalidCredentials: {
error: {
errors: [
{
domain: 'global',
reason: 'authError',
message: 'Invalid Credentials',
locationType: 'header',
location: 'Authorization',
},
],
code: 401,
message: 'Invalid Credentials',
},
code: '401',
message: 'authError',
},
// 404 Not Found
notFound: {
error: {
errors: [
{
domain: 'global',
reason: 'notFound',
message: 'Resource not found: userId',
location: 'userId',
locationType: 'parameter',
},
],
code: 404,
message: 'Resource not found: userId',
},
code: '404',
message: 'notFound',
},
// 410 Gone
gone: {
error: {
errors: [
{
domain: 'global',
reason: 'resourceGone',
message: 'Resource has been deleted',
location: 'messageId',
locationType: 'parameter',
},
],
code: 410,
message: 'Resource has been deleted',
},
code: '410',
message: 'resourceGone',
},
// 403 Daily Limit Exceeded
dailyLimitExceeded: {
error: {
errors: [
{
domain: 'usageLimits',
reason: 'dailyLimitExceeded',
message: 'Daily Limit Exceeded',
},
],
code: 403,
message: 'Daily Limit Exceeded',
},
code: '403',
message: 'dailyLimitExceeded',
},
// 403 User Rate Limit Exceeded
userRateLimitExceeded: {
error: {
errors: [
{
domain: 'usageLimits',
reason: 'userRateLimitExceeded',
message: 'User Rate Limit Exceeded',
},
],
code: 403,
message: 'User Rate Limit Exceeded',
},
code: '403',
message: 'userRateLimitExceeded',
},
// 403 Rate Limit Exceeded
rateLimitExceeded: {
error: {
errors: [
{
domain: 'usageLimits',
reason: 'rateLimitExceeded',
message: 'Rate Limit Exceeded',
},
],
code: 403,
message: 'Rate Limit Exceeded',
},
code: '403',
message: 'rateLimitExceeded',
},
// 403 Domain Policy Error
domainPolicyError: {
error: {
errors: [
{
domain: 'global',
reason: 'domainPolicy',
message: 'The domain administrators have disabled Gmail apps.',
},
],
code: 403,
message: 'The domain administrators have disabled Gmail apps.',
},
code: '403',
message: 'domainPolicy',
},
// 429 Too Many Requests (Concurrent Requests)
tooManyConcurrentRequests: {
error: {
errors: [
{
domain: 'global',
reason: 'rateLimitExceeded',
message: 'Too many concurrent requests for user',
},
],
code: 429,
message: 'Too many concurrent requests for user',
},
code: '429',
message: 'tooManyConcurrentRequests',
},
// 500 Backend Error
backendError: {
error: {
errors: [
{
domain: 'global',
reason: 'backendError',
message: 'Backend Error',
},
],
code: 500,
message: 'Backend Error',
},
code: '500',
message: 'backendError',
},
getError: function (code: number, type?: string) {
getError: function (code: number, type?: string): GmailApiError {
switch (code) {
case 400:
switch (type) {
@@ -0,0 +1,217 @@
import { type GmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type';
const gmailBatchApiErrorMocks = {
// 400 Bad Request - Invalid query parameters
badRequest: {
code: 400,
errors: [
{
domain: 'global',
location: 'orderBy',
locationType: 'parameter',
message:
'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.',
reason: 'badRequest',
},
],
message:
'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.',
},
// 400 Invalid Grant
invalidGrant: {
code: 400,
errors: [
{
domain: 'global',
reason: 'invalid_grant',
message: 'Invalid Credentials',
},
],
message: 'Invalid Credentials',
},
// 400 Failed Precondition
failedPrecondition: {
code: 400,
errors: [
{
domain: 'global',
reason: 'failedPrecondition',
message: 'Failed Precondition',
},
],
message: 'Failed Precondition',
},
// 401 Invalid Credentials
invalidCredentials: {
errors: [
{
domain: 'global',
reason: 'authError',
message: 'Invalid Credentials',
locationType: 'header',
location: 'Authorization',
},
],
code: 401,
message: 'Invalid Credentials',
},
// 404 Not Found
notFound: {
errors: [
{
domain: 'global',
reason: 'notFound',
message: 'Resource not found: userId',
location: 'userId',
locationType: 'parameter',
},
],
code: 404,
message: 'Resource not found: userId',
},
// 410 Gone
gone: {
errors: [
{
domain: 'global',
reason: 'resourceGone',
message: 'Resource has been deleted',
location: 'messageId',
locationType: 'parameter',
},
],
code: 410,
message: 'Resource has been deleted',
},
// 403 Daily Limit Exceeded
dailyLimitExceeded: {
errors: [
{
domain: 'usageLimits',
reason: 'dailyLimitExceeded',
message: 'Daily Limit Exceeded',
},
],
code: 403,
message: 'Daily Limit Exceeded',
},
// 403 User Rate Limit Exceeded
userRateLimitExceeded: {
errors: [
{
domain: 'usageLimits',
reason: 'userRateLimitExceeded',
message: 'User Rate Limit Exceeded',
},
],
code: 403,
message: 'User Rate Limit Exceeded',
},
// 403 Rate Limit Exceeded
rateLimitExceeded: {
errors: [
{
domain: 'usageLimits',
reason: 'rateLimitExceeded',
message: 'Rate Limit Exceeded',
},
],
code: 403,
message: 'Rate Limit Exceeded',
},
// 403 Domain Policy Error
domainPolicyError: {
errors: [
{
domain: 'global',
reason: 'domainPolicy',
message: 'The domain administrators have disabled Gmail apps.',
},
],
code: 403,
message: 'The domain administrators have disabled Gmail apps.',
},
// 429 Too Many Requests (Concurrent Requests)
tooManyConcurrentRequests: {
errors: [
{
domain: 'global',
reason: 'rateLimitExceeded',
message: 'Too many concurrent requests for user',
},
],
code: 429,
message: 'Too many concurrent requests for user',
},
// 500 Backend Error
backendError: {
errors: [
{
domain: 'global',
reason: 'backendError',
message: 'Backend Error',
},
],
code: 500,
message: 'Backend Error',
},
getError: function (code: number, type?: string): GmailApiBatchError {
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 gmailBatchApiErrorMocks;
@@ -1,26 +1,33 @@
import { Injectable, Logger } from '@nestjs/common';
import { GmailNetworkErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-network-error-handler.service';
import { parseGmailMessageListFetchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-message-list-fetch-error.util';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { isGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-error-error.util';
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
import { parseGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util';
import { parseGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-network-error.util';
@Injectable()
export class GmailMessageListFetchErrorHandler {
private readonly logger = new Logger(GmailMessageListFetchErrorHandler.name);
constructor(
private readonly gmailNetworkErrorHandler: GmailNetworkErrorHandler,
) {}
constructor() {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public handleError(error: any): void {
this.logger.log(`Error fetching message list`, error);
const networkError = this.gmailNetworkErrorHandler.handleError(error);
if (networkError) {
throw networkError;
public handleError(error: unknown): void {
this.logger.error(`Gmail: Error fetching message list: ${error}`);
if (isGmailNetworkError(error)) {
throw parseGmailNetworkError(error);
}
throw parseGmailMessageListFetchError(error, { cause: error });
if (isGmailApiError(error)) {
throw parseGmailApiError(error);
}
throw new MessageImportDriverException(
'Unknown error',
MessageImportDriverExceptionCode.UNKNOWN,
);
}
}
@@ -1,32 +1,34 @@
import { Injectable, Logger } from '@nestjs/common';
import { GmailNetworkErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-network-error-handler.service';
import { parseGmailMessagesImportError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-messages-import-error.util';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { isGmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-batch-error.util';
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
import { parseGmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-batch-error.util';
import { parseGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-network-error.util';
@Injectable()
export class GmailMessagesImportErrorHandler {
private readonly logger = new Logger(GmailMessagesImportErrorHandler.name);
constructor(
private readonly gmailNetworkErrorHandler: GmailNetworkErrorHandler,
) {}
constructor() {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public handleError(error: any, messageExternalId: string): void {
this.logger.log(`Error fetching messages`, error);
public handleError(error: unknown, messageExternalId: string): void {
this.logger.error(`Gmail: Error importing messages: ${error}`);
const networkError = this.gmailNetworkErrorHandler.handleError(error);
if (networkError) {
throw networkError;
if (isGmailNetworkError(error)) {
throw parseGmailNetworkError(error);
}
const gmailError = parseGmailMessagesImportError(error, messageExternalId, {
cause: error,
});
if (gmailError) {
throw gmailError;
if (isGmailApiBatchError(error)) {
throw parseGmailApiBatchError(error, messageExternalId);
}
throw new MessageImportDriverException(
'Unknown error',
MessageImportDriverExceptionCode.UNKNOWN,
);
}
}
@@ -1,23 +0,0 @@
import { Injectable } from '@nestjs/common';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { isAxiosTemporaryError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-axios-gaxios-error.util';
@Injectable()
export class GmailNetworkErrorHandler {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public handleError(error: any): MessageImportDriverException | null {
if (isAxiosTemporaryError(error)) {
return new MessageImportDriverException(
error.message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: error },
);
}
return null;
}
}
@@ -0,0 +1,7 @@
export type GmailApiBatchError = {
code: number;
errors: {
reason: string;
message: string;
}[];
};
@@ -0,0 +1,4 @@
export type GmailApiError = {
code: string;
message: string;
};
@@ -1,45 +1,45 @@
import gaxiosErrorMocks from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gaxios-error-mocks';
import { isAxiosTemporaryError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-axios-gaxios-error.util';
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
describe('parseGaxiosError', () => {
describe('isGmailNetworkError', () => {
it('should return a MessageImportDriverException for ECONNRESET', () => {
const error = gaxiosErrorMocks.getError('ECONNRESET');
const result = isAxiosTemporaryError(error);
const result = isGmailNetworkError(error);
expect(result).toBe(true);
});
it('should return a MessageImportDriverException for ENOTFOUND', () => {
const error = gaxiosErrorMocks.getError('ENOTFOUND');
const result = isAxiosTemporaryError(error);
const result = isGmailNetworkError(error);
expect(result).toBe(true);
});
it('should return a MessageImportDriverException for ECONNABORTED', () => {
const error = gaxiosErrorMocks.getError('ECONNABORTED');
const result = isAxiosTemporaryError(error);
const result = isGmailNetworkError(error);
expect(result).toBe(true);
});
it('should return a MessageImportDriverException for ETIMEDOUT', () => {
const error = gaxiosErrorMocks.getError('ETIMEDOUT');
const result = isAxiosTemporaryError(error);
const result = isGmailNetworkError(error);
expect(result).toBe(true);
});
it('should return a MessageImportDriverException for ERR_NETWORK', () => {
const error = gaxiosErrorMocks.getError('ERR_NETWORK');
const result = isAxiosTemporaryError(error);
const result = isGmailNetworkError(error);
expect(result).toBe(true);
});
it('should return undefined for unknown error codes', () => {
const error = { code: 'UNKNOWN_ERROR' } as any;
const result = isAxiosTemporaryError(error);
const result = isGmailNetworkError(error);
expect(result).toBe(false);
});
@@ -3,12 +3,12 @@ import {
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 { parseGmailMessageListFetchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-message-list-fetch-error.util';
import { parseGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util';
describe('parseGmailMessageListFetchError', () => {
describe('parseGmailApiError', () => {
it('should handle 400 Bad Request', () => {
const error = gmailApiErrorMocks.getError(400);
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(MessageImportDriverExceptionCode.UNKNOWN);
@@ -16,7 +16,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 400 Invalid Grant', () => {
const error = gmailApiErrorMocks.getError(400, 'invalid_grant');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -26,7 +26,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 400 Failed Precondition', () => {
const error = gmailApiErrorMocks.getError(400, 'failedPrecondition');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -36,7 +36,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 401 Invalid Credentials', () => {
const error = gmailApiErrorMocks.getError(401);
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -46,7 +46,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 403 Daily Limit Exceeded', () => {
const error = gmailApiErrorMocks.getError(403, 'dailyLimit');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -56,7 +56,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 403 User Rate Limit Exceeded', () => {
const error = gmailApiErrorMocks.getError(403, 'userRateLimit');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -66,7 +66,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 403 Rate Limit Exceeded', () => {
const error = gmailApiErrorMocks.getError(403, 'rateLimit');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -76,7 +76,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 403 Domain Policy Error', () => {
const error = gmailApiErrorMocks.getError(403, 'domainPolicy');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -86,7 +86,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 404 as sync cursor error', () => {
const error = gmailApiErrorMocks.getError(404);
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -96,7 +96,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 410 Gone', () => {
const error = gmailApiErrorMocks.getError(410);
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(MessageImportDriverExceptionCode.UNKNOWN);
@@ -104,7 +104,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 429 Too Many Requests', () => {
const error = gmailApiErrorMocks.getError(429, 'concurrent');
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -114,7 +114,7 @@ describe('parseGmailMessageListFetchError', () => {
it('should handle 500 Backend Error', () => {
const error = gmailApiErrorMocks.getError(500);
const exception = parseGmailMessageListFetchError(error.error);
const exception = parseGmailApiError(error);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception.code).toBe(
@@ -2,48 +2,39 @@ 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 { parseGmailMessagesImportError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-messages-import-error.util';
import { default as gmailBatchApiErrorMocks } from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-batch-api-error-mocks';
import { parseGmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-batch-error.util';
const messageExternalId = '123';
describe('parseGmailMessagesImportError', () => {
describe('parseGmailApiBatchError', () => {
it('should handle 400 Bad Request', () => {
const error = gmailApiErrorMocks.getError(400);
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(400);
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(MessageImportDriverExceptionCode.UNKNOWN);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 400 Invalid Grant', () => {
const error = gmailApiErrorMocks.getError(400, 'invalid_grant');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(400, 'invalid_grant');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 400 Failed Precondition', () => {
const error = gmailApiErrorMocks.getError(400, 'failedPrecondition');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(400, 'failedPrecondition');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
@@ -52,146 +43,119 @@ describe('parseGmailMessagesImportError', () => {
});
it('should handle 401 Invalid Credentials', () => {
const error = gmailApiErrorMocks.getError(401);
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(401);
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 403 Daily Limit Exceeded', () => {
const error = gmailApiErrorMocks.getError(403, 'dailyLimit');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(403, 'dailyLimit');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 403 User Rate Limit Exceeded', () => {
const error = gmailApiErrorMocks.getError(403, 'userRateLimit');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(403, 'userRateLimit');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 403 Rate Limit Exceeded', () => {
const error = gmailApiErrorMocks.getError(403, 'rateLimit');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(403, 'rateLimit');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 403 Domain Policy Error', () => {
const error = gmailApiErrorMocks.getError(403, 'domainPolicy');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(403, 'domainPolicy');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 404 Not Found', () => {
const error = gmailApiErrorMocks.getError(404);
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(404);
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 410 Gone', () => {
const error = gmailApiErrorMocks.getError(410);
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(410);
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 429 Too Many Requests', () => {
const error = gmailApiErrorMocks.getError(429, 'concurrent');
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(429, 'concurrent');
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
it('should handle 500 Backend Error', () => {
const error = gmailApiErrorMocks.getError(500);
const exception = parseGmailMessagesImportError(
error.error,
messageExternalId,
);
const error = gmailBatchApiErrorMocks.getError(500);
const exception = parseGmailApiBatchError(error, messageExternalId);
expect(exception).toBeInstanceOf(MessageImportDriverException);
expect(exception?.code).toBe(
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
);
expect(exception?.message).toBe(
`${error.error.errors[0].message} for message with externalId: ${messageExternalId}`,
`${error.errors[0].message} for message with externalId: ${messageExternalId}`,
);
});
});
@@ -0,0 +1,21 @@
import { type GmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type';
export const isGmailApiBatchError = (
error: unknown,
): error is GmailApiBatchError => {
if (error === null || typeof error !== 'object') {
return false;
}
if (
!('code' in error) ||
!('errors' in error) ||
!Array.isArray(error.errors) ||
error.errors.length === 0 ||
error.errors.some((error) => !('reason' in error) || !('message' in error))
) {
return false;
}
return true;
};
@@ -0,0 +1,18 @@
import { type GmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-error.type';
export const isGmailApiError = (error: unknown): error is GmailApiError => {
if (error === null || typeof error !== 'object') {
return false;
}
if (
!('code' in error) ||
typeof error.code !== 'string' ||
!('message' in error) ||
typeof error.message !== 'string'
) {
return false;
}
return true;
};
@@ -2,10 +2,16 @@ import { type GaxiosError } from 'gaxios';
import { MessageNetworkExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-network.exception';
export const isAxiosTemporaryError = (error: GaxiosError): boolean => {
const { code } = error;
export const isGmailNetworkError = (error: unknown): error is GaxiosError => {
if (error === null || typeof error !== 'object') {
return false;
}
switch (code) {
if (!('code' in error)) {
return false;
}
switch (error.code) {
case MessageNetworkExceptionCode.ECONNRESET:
case MessageNetworkExceptionCode.ENOTFOUND:
case MessageNetworkExceptionCode.ECONNABORTED:
@@ -13,7 +19,6 @@ export const isAxiosTemporaryError = (error: GaxiosError): boolean => {
case MessageNetworkExceptionCode.ERR_NETWORK:
case MessageNetworkExceptionCode.EHOSTUNREACH:
return true;
default:
return false;
}
@@ -2,17 +2,11 @@ import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { type GmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type';
export const parseGmailMessagesImportError = (
error: {
code?: number;
errors: {
reason: string;
message: string;
}[];
},
messageExternalId: string,
options?: { cause?: Error },
export const parseGmailApiBatchError = (
error: GmailApiBatchError,
messageExternalId?: string,
): MessageImportDriverException | undefined => {
const { code, errors } = error;
@@ -26,7 +20,6 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
}
if (reason === 'failedPrecondition') {
@@ -34,21 +27,18 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
}
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
}
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.UNKNOWN,
{ cause: options?.cause },
);
case 404:
@@ -56,14 +46,12 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR,
{ cause: options?.cause },
);
case 429:
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
case 403:
@@ -75,14 +63,12 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
}
if (reason === 'domainPolicy') {
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
}
@@ -92,14 +78,12 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
case 503:
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
case 500:
@@ -109,7 +93,6 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
}
@@ -117,7 +100,6 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
`${code} - ${reason} - ${message}`,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
}
break;
@@ -129,6 +111,5 @@ export const parseGmailMessagesImportError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.UNKNOWN,
{ cause: options?.cause },
);
};
@@ -2,119 +2,98 @@ import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { type GmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-error.type';
export const parseGmailMessageListFetchError = (
error: {
code?: number;
errors: {
reason: string;
message: string;
}[];
},
options?: { cause?: Error },
export const parseGmailApiError = (
error: GmailApiError,
): MessageImportDriverException => {
const { code, errors } = error;
const reason = errors?.[0]?.reason;
const message = errors?.[0]?.message;
const { code, message } = error;
switch (code) {
case 400:
if (reason === 'invalid_grant') {
case '400':
if (message === 'invalid_grant') {
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
}
if (reason === 'failedPrecondition') {
if (message === 'failedPrecondition') {
if (message.includes('Mail service not enabled')) {
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
}
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
}
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.UNKNOWN,
{ cause: options?.cause },
);
case 404:
case '404':
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.SYNC_CURSOR_ERROR,
{ cause: options?.cause },
);
case 429:
case '429':
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
case 403:
case '403':
if (
reason === 'rateLimitExceeded' ||
reason === 'userRateLimitExceeded' ||
reason === 'dailyLimitExceeded'
message === 'rateLimitExceeded' ||
message === 'userRateLimitExceeded' ||
message === 'dailyLimitExceeded'
) {
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
}
if (reason === 'domainPolicy') {
if (message === 'domainPolicy') {
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
}
break;
case 401:
case '401':
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
case 503:
case '503':
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
case 500:
case 502:
case 504:
if (reason === 'backendError') {
case '500':
case '502':
case '504':
if (message === 'backendError') {
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
}
if (errors?.[0]?.message.includes(`Authentication backend unavailable`)) {
if (message.includes(`Authentication backend unavailable`)) {
return new MessageImportDriverException(
`${code} - ${reason} - ${message}`,
`${code} - ${message}`,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: options?.cause },
);
}
break;
@@ -126,6 +105,5 @@ export const parseGmailMessageListFetchError = (
return new MessageImportDriverException(
message,
MessageImportDriverExceptionCode.UNKNOWN,
{ cause: options?.cause },
);
};
@@ -0,0 +1,16 @@
import { type GaxiosError } from 'gaxios';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
export const parseGmailNetworkError = (
error: GaxiosError,
): MessageImportDriverException | null => {
return new MessageImportDriverException(
error.message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
{ cause: error },
);
};