From 5b1d6206e56dfdd37a972f972f3e2db27eb3c7f4 Mon Sep 17 00:00:00 2001 From: Rico Furtado Date: Wed, 3 Jun 2026 15:17:47 -0400 Subject: [PATCH 1/3] fix: enhance Google Drive OAuth error handling and re-authentication logic --- src/api/connectors.py | 13 +++++++++++++ src/connectors/google_drive/oauth.py | 11 ++++++++++- .../test_reconcile_orphans_for_connector_type.py | 2 ++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/api/connectors.py b/src/api/connectors.py index 665962d87..5a7532b25 100644 --- a/src/api/connectors.py +++ b/src/api/connectors.py @@ -193,6 +193,18 @@ async def compute_orphans_for_connector_type( ) return None + # Re-authenticate to refresh any stale cached credentials before + # making API calls. get_connector() may return a cached connector + # whose access token has since expired (Google tokens last 1 hour). + # This mirrors the pattern used in connector_sync. + if not await connector.authenticate(): + logger.info( + "Skipping orphan compute — re-authentication failed", + connector_type=connector_type, + connection_id=conn.connection_id, + ) + return None + # Drive the per-id existence check via cfg.file_ids when the # connector supports it (SharePoint / OneDrive / Google Drive). # The flat default of list_files() only returns the *root* listing @@ -235,6 +247,7 @@ async def compute_orphans_for_connector_type( connector_type=connector_type, connection_id=conn.connection_id, error=str(e), + error_type=type(e).__name__, ) return None diff --git a/src/connectors/google_drive/oauth.py b/src/connectors/google_drive/oauth.py index 1d1e88bb2..b8c7617aa 100644 --- a/src/connectors/google_drive/oauth.py +++ b/src/connectors/google_drive/oauth.py @@ -129,7 +129,16 @@ async def load_credentials(self) -> Credentials | None: except Exception as e: logger.debug("[GoogleDrive] load_credentials: token refresh failed: %s", e) self.creds = None - self._remove_token_file() + # Only wipe the token file for permanent failures (revoked or + # invalid grant). Transient failures (network timeout, etc.) + # should not force the user through re-authentication. + error_str = str(e).lower() + is_permanent = any( + k in error_str + for k in ("invalid_grant", "revoked", "unauthorized_client") + ) + if is_permanent: + self._remove_token_file() raise ValueError( f"Failed to refresh Google Drive credentials. " f"The refresh token may have expired or been revoked. " diff --git a/tests/unit/api/test_reconcile_orphans_for_connector_type.py b/tests/unit/api/test_reconcile_orphans_for_connector_type.py index 9e339ca37..8ffac1095 100644 --- a/tests/unit/api/test_reconcile_orphans_for_connector_type.py +++ b/tests/unit/api/test_reconcile_orphans_for_connector_type.py @@ -30,6 +30,7 @@ def _make_connection(connection_id: str, is_active: bool = True): def _make_connector(remote_file_ids, *, authenticated=True, raise_on_list=False): connector = MagicMock() connector.is_authenticated = authenticated + connector.authenticate = AsyncMock(return_value=authenticated) if raise_on_list: connector.list_files = AsyncMock(side_effect=RuntimeError("graph 503")) else: @@ -315,6 +316,7 @@ async def test_paginated_listing_aggregates_all_pages(): conn = _make_connection("c1") connector = MagicMock() connector.is_authenticated = True + connector.authenticate = AsyncMock(return_value=True) pages = [ {"files": [{"id": "a"}], "nextPageToken": "tok-1"}, {"files": [{"id": "b"}, {"id": "c"}]}, From 04d9f81889bd5fb4f89cc4143d7009113d9dfc58 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:18:49 +0000 Subject: [PATCH 2/3] style: ruff autofix (auto) --- src/connectors/google_drive/oauth.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/connectors/google_drive/oauth.py b/src/connectors/google_drive/oauth.py index b8c7617aa..53c5645e8 100644 --- a/src/connectors/google_drive/oauth.py +++ b/src/connectors/google_drive/oauth.py @@ -134,8 +134,7 @@ async def load_credentials(self) -> Credentials | None: # should not force the user through re-authentication. error_str = str(e).lower() is_permanent = any( - k in error_str - for k in ("invalid_grant", "revoked", "unauthorized_client") + k in error_str for k in ("invalid_grant", "revoked", "unauthorized_client") ) if is_permanent: self._remove_token_file() From 0824f579350bd404cd6a6a2861221a328a4bf2ec Mon Sep 17 00:00:00 2001 From: Rico Furtado Date: Thu, 4 Jun 2026 00:39:56 -0400 Subject: [PATCH 3/3] fix: improve Google Drive OAuth re-authentication logic for orphan computation --- src/api/connectors.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/api/connectors.py b/src/api/connectors.py index 5a7532b25..61ef852ad 100644 --- a/src/api/connectors.py +++ b/src/api/connectors.py @@ -185,7 +185,7 @@ async def compute_orphans_for_connector_type( for conn in active: try: connector = await connector_service.get_connector(conn.connection_id) - if not connector or not connector.is_authenticated: + if not connector: logger.info( "Skipping orphan compute — connection unauthenticated", connector_type=connector_type, @@ -193,9 +193,8 @@ async def compute_orphans_for_connector_type( ) return None - # Re-authenticate to refresh any stale cached credentials before - # making API calls. get_connector() may return a cached connector - # whose access token has since expired (Google tokens last 1 hour). + # Always re-authenticate before making API calls so that stale + # cached access tokens (Google tokens last 1 hour) are refreshed. # This mirrors the pattern used in connector_sync. if not await connector.authenticate(): logger.info(