Fix messaging errors on message folder list (#16404)

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.
This commit is contained in:
Charles Bochet
2025-12-09 17:01:25 +01:00
committed by GitHub
parent a18203934c
commit aa519e7d81
5 changed files with 347 additions and 84 deletions
@@ -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}`);
}
};
@@ -1,4 +1,4 @@
export type GmailApiError = {
code: string;
code: number | string;
message: string;
};
@@ -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);
@@ -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'
) {
@@ -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,