refactor google refresh token service error handling (#17127)

Refactors to be consistent with Microsoft service
Handles scenarios like temporary error which was not handled before
Moved `IsGmailNetworkError` from root orchestrator to driver level
This commit is contained in:
neo773
2026-01-13 23:54:53 +05:30
committed by GitHub
parent f793faae2b
commit b79056b3a2
4 changed files with 90 additions and 21 deletions
@@ -1,6 +1,5 @@
import { Injectable } from '@nestjs/common';
import { GaxiosError } from 'gaxios';
import { google } from 'googleapis';
import { isDefined } from 'twenty-shared/utils';
@@ -10,6 +9,7 @@ import {
ConnectedAccountRefreshAccessTokenExceptionCode,
} from 'src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception';
import { type ConnectedAccountTokens } from 'src/modules/connected-account/refresh-tokens-manager/services/connected-account-refresh-tokens.service';
import { parseGoogleOAuthError } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/utils/parse-google-oauth-error.util';
@Injectable()
export class GoogleAPIRefreshAccessTokenService {
@@ -39,17 +39,11 @@ export class GoogleAPIRefreshAccessTokenService {
refreshToken,
};
} catch (error) {
if (
error instanceof GaxiosError &&
error.response?.data?.error === 'invalid_grant'
) {
throw new ConnectedAccountRefreshAccessTokenException(
'Error refreshing Google tokens: Invalid refresh token',
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
);
if (error instanceof ConnectedAccountRefreshAccessTokenException) {
throw error;
}
throw error;
throw parseGoogleOAuthError(error);
}
}
}
@@ -0,0 +1,82 @@
import { type GaxiosError } from 'gaxios';
import {
ConnectedAccountRefreshAccessTokenException,
ConnectedAccountRefreshAccessTokenExceptionCode,
} from 'src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception';
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
export const parseGoogleOAuthError = (
error: unknown,
): ConnectedAccountRefreshAccessTokenException => {
if (isGmailNetworkError(error)) {
return new ConnectedAccountRefreshAccessTokenException(
`Google refresh token network error: ${error.code} - ${error.message}`,
ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR,
);
}
const gaxiosError = error as GaxiosError;
const googleOAuthError = {
code: gaxiosError.response?.status,
reason:
gaxiosError.response?.data?.error ||
gaxiosError.response?.data?.error_description ||
'Unknown reason',
message:
gaxiosError.response?.data?.error_description ||
gaxiosError.message ||
'Unknown error',
};
switch (googleOAuthError.code) {
case 400:
if (googleOAuthError.reason === 'invalid_grant') {
return new ConnectedAccountRefreshAccessTokenException(
googleOAuthError.message,
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
);
}
return new ConnectedAccountRefreshAccessTokenException(
googleOAuthError.message,
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
);
case 401:
return new ConnectedAccountRefreshAccessTokenException(
googleOAuthError.message,
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
);
case 403:
return new ConnectedAccountRefreshAccessTokenException(
googleOAuthError.message,
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
);
case 429:
return new ConnectedAccountRefreshAccessTokenException(
googleOAuthError.message,
ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR,
);
case 500:
case 502:
case 503:
case 504:
return new ConnectedAccountRefreshAccessTokenException(
`${googleOAuthError.code} - ${googleOAuthError.message}`,
ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR,
);
default:
break;
}
return new ConnectedAccountRefreshAccessTokenException(
`Google refresh token failed: ${googleOAuthError.message}`,
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
);
};
@@ -275,9 +275,10 @@ describe('ConnectedAccountRefreshTokensService', () => {
lastCredentialsRefreshedAt: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
} as ConnectedAccountWorkspaceEntity;
const networkError = new Error('Network error');
(networkError as any).code = 'ECONNRESET';
const networkError = new ConnectedAccountRefreshAccessTokenException(
'Google refresh token network error: ECONNRESET - Network error',
ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR,
);
jest
.spyOn(googleAPIRefreshAccessTokenService, 'refreshTokens')
@@ -12,7 +12,6 @@ import {
ConnectedAccountRefreshAccessTokenExceptionCode,
} from 'src/modules/connected-account/refresh-tokens-manager/exceptions/connected-account-refresh-tokens.exception';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
export type ConnectedAccountTokens = {
accessToken: string;
@@ -157,13 +156,6 @@ export class ConnectedAccountRefreshTokensService {
);
}
} catch (error) {
if (isGmailNetworkError(error)) {
throw new ConnectedAccountRefreshAccessTokenException(
`Error refreshing tokens for connected account ${connectedAccount.id} in workspace ${workspaceId}: ${error.code}`,
ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR,
);
}
this.logger.log(
`Error while refreshing tokens on connected account ${connectedAccount.id} in workspace ${workspaceId}`,
error,