Classify OAuth refresh errors by reason instead of status code (#23705)

Both provider parsers treated unrecognised failures as permanent, so a
single transient error marked a working account as needing reconnection
and it never recovered on its own. Permanence is now decided by the
provider's OAuth error code, everything else is temporary and retries.

Checked against prod: 15 connected accounts currently flagged
auth-failed still return a valid token when refreshed, and 12 of those
were flagged in bursts across unrelated workspaces (five within 90
seconds on 2026-01-13), which points at a transient blip rather than
users revoking access.

---------

Co-authored-by: neo773 <huzef@twenty.com>
This commit is contained in:
neo773
2026-08-03 17:13:00 +05:30
committed by GitHub
parent 37f1fe17ab
commit 00ad1544d8
4 changed files with 40 additions and 108 deletions
@@ -0,0 +1,11 @@
/**
* @see https://developers.google.com/identity/protocols/oauth2/web-server#authorization-errors
*/
export const GOOGLE_PERMANENT_OAUTH_ERROR_CODES = new Set([
'invalid_grant',
'invalid_client',
'unauthorized_client',
'unsupported_grant_type',
'invalid_scope',
'admin_policy_enforced',
]);
@@ -4,6 +4,7 @@ import {
ConnectedAccountRefreshAccessTokenException,
ConnectedAccountRefreshAccessTokenExceptionCode,
} from 'src/engine/metadata-modules/connected-account/exceptions/connected-account-refresh-tokens.exception';
import { GOOGLE_PERMANENT_OAUTH_ERROR_CODES } from 'src/modules/connected-account/refresh-tokens-manager/drivers/google/constants/google-permanent-oauth-error-codes.constant';
import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util';
export const parseGoogleOAuthError = (
@@ -20,63 +21,22 @@ export const parseGoogleOAuthError = (
const googleOAuthError = {
code: gaxiosError.response?.status,
reason:
gaxiosError.response?.data?.error ||
gaxiosError.response?.data?.error_description ||
'Unknown reason',
reason: gaxiosError.response?.data?.error || '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;
if (GOOGLE_PERMANENT_OAUTH_ERROR_CODES.has(googleOAuthError.reason)) {
return new ConnectedAccountRefreshAccessTokenException(
`Google auth error: ${googleOAuthError.reason} - ${googleOAuthError.message}`,
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
);
}
return new ConnectedAccountRefreshAccessTokenException(
`Google refresh token failed: ${googleOAuthError.message}`,
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
`Google refresh token failed (${googleOAuthError.code ?? 'no status'}): ${googleOAuthError.reason} - ${googleOAuthError.message}`,
ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR,
);
};
@@ -0,0 +1,9 @@
/**
* @see https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes
*/
export const MICROSOFT_PERMANENT_AUTH_ERROR_CODES = new Set([
'invalid_grant',
'invalid_client',
'unauthorized_client',
'invalid_request',
]);
@@ -1,34 +1,10 @@
import {
AuthError,
InteractionRequiredAuthError,
ServerError,
} from '@azure/msal-node';
import { AuthError, InteractionRequiredAuthError } from '@azure/msal-node';
import {
ConnectedAccountRefreshAccessTokenException,
ConnectedAccountRefreshAccessTokenExceptionCode,
} from 'src/engine/metadata-modules/connected-account/exceptions/connected-account-refresh-tokens.exception';
/**
* @see https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes
*/
const PERMANENT_AUTH_ERROR_CODES = new Set([
'invalid_grant',
'invalid_client',
'unauthorized_client',
'invalid_request',
]);
/**
* @see https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/src/error/ClientAuthErrorCodes.ts
*/
const TRANSIENT_AUTH_ERROR_CODES = new Set([
'network_error',
'no_network_connectivity',
'endpoints_resolution_error',
'openid_config_error',
'request_cannot_be_made',
]);
import { MICROSOFT_PERMANENT_AUTH_ERROR_CODES } from 'src/modules/connected-account/refresh-tokens-manager/drivers/microsoft/constants/microsoft-permanent-auth-error-codes.constant';
export const parseMsalError = (
error: unknown,
@@ -40,44 +16,20 @@ export const parseMsalError = (
);
}
if (error instanceof ServerError) {
const status = error.status;
if (status === 429) {
return new ConnectedAccountRefreshAccessTokenException(
'Microsoft rate limit exceeded',
ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR,
);
}
if (status && status >= 500 && status < 600) {
return new ConnectedAccountRefreshAccessTokenException(
`Microsoft server error (${status}): ${error.errorMessage}`,
ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR,
);
}
}
if (error instanceof AuthError) {
if (TRANSIENT_AUTH_ERROR_CODES.has(error.errorCode)) {
return new ConnectedAccountRefreshAccessTokenException(
`Microsoft network error: ${error.errorCode} - ${error.errorMessage}`,
ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR,
);
}
if (PERMANENT_AUTH_ERROR_CODES.has(error.errorCode)) {
return new ConnectedAccountRefreshAccessTokenException(
`Microsoft auth error: ${error.errorCode} - ${error.errorMessage}`,
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
);
}
if (
error instanceof AuthError &&
MICROSOFT_PERMANENT_AUTH_ERROR_CODES.has(error.errorCode)
) {
return new ConnectedAccountRefreshAccessTokenException(
`Microsoft auth error: ${error.errorCode} - ${error.errorMessage}`,
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
);
}
const message = error instanceof Error ? error.message : String(error);
return new ConnectedAccountRefreshAccessTokenException(
`Microsoft token refresh failed: ${message}`,
ConnectedAccountRefreshAccessTokenExceptionCode.INVALID_REFRESH_TOKEN,
ConnectedAccountRefreshAccessTokenExceptionCode.TEMPORARY_NETWORK_ERROR,
);
};