diff --git a/src/api/connectors.py b/src/api/connectors.py index 665962d87..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,6 +193,17 @@ async def compute_orphans_for_connector_type( ) return None + # 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( + "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 +246,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..53c5645e8 100644 --- a/src/connectors/google_drive/oauth.py +++ b/src/connectors/google_drive/oauth.py @@ -129,7 +129,15 @@ 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"}]},