From a92e93d6175410f4a156f9acce72369f2d7dbce2 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2026 00:46:29 +0000 Subject: [PATCH] fix(warehouse-sources): classify exhausted HubSpot 401 as reconnect A revoked HubSpot OAuth grant surfaced as an opaque failed sync and a tracked exception. On a 401, every fetch loop refreshes the access token and re-raises HubspotRetryableError; after tenacity's 5 attempts the message reached neither classification map, so it fell through to logger.aexception. Map the shared "401 - refreshed token, retrying" fragment to the existing reconnect copy in get_non_retryable_errors(). The old "401 Client Error: Unauthorized" entry was unreachable because the 401 branch re-raises before raise_for_hubspot_status runs. Generated-By: PostHog Desktop Task-Id: 88a0222b-a750-417f-802c-f4475071c741 --- .../data_imports/sources/hubspot/source.py | 14 ++++++++------ .../sources/hubspot/test/test_source_routing.py | 10 +++++++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/products/warehouse_sources/backend/temporal/data_imports/sources/hubspot/source.py b/products/warehouse_sources/backend/temporal/data_imports/sources/hubspot/source.py index edb204f1f602..08286dce4bc9 100644 --- a/products/warehouse_sources/backend/temporal/data_imports/sources/hubspot/source.py +++ b/products/warehouse_sources/backend/temporal/data_imports/sources/hubspot/source.py @@ -132,12 +132,14 @@ def get_non_retryable_errors(self) -> dict[str, str | None]: **missing_scope_errors, "missing or invalid refresh token": "Your HubSpot connection is invalid or expired. Please reconnect it.", "missing or unknown hub id": None, - # HubSpot's CRM API returns 401/403 when the OAuth grant can't read the requested object - # (token revoked, or the connected app is missing a scope like `crm.objects.companies.read`). - # `fetch_data` already refreshes the access token once on a 401; if the retried request is - # still rejected, the credentials genuinely lack access and retrying can't recover. Match the - # stable host, not the per-object URL path (companies/deals/contacts/...), which varies. - "401 Client Error: Unauthorized for url: https://api.hubapi.com": "Your HubSpot credentials are no longer authorized. Please reconnect your HubSpot account and ensure it has the required permissions, then try again.", + # A 401 means the OAuth grant can't read the requested object (token revoked, or the + # connected app lost a scope like `crm.objects.companies.read`). Every fetch loop + # (fetch_data._get, fetch_page, v4 associations, search) refreshes the access token on a + # 401 and re-raises HubspotRetryableError, so tenacity retries with a fresh token. Five + # straight 401s after a good refresh means the grant is dead, not a transient blip, so + # retrying can't recover. Match the shared message fragment all four loops emit, not the + # per-loop prefix or the volatile URL. + "401 - refreshed token, retrying": "Your HubSpot credentials are no longer authorized. Please reconnect your HubSpot account and ensure it has the required permissions, then try again.", "403 Client Error: Forbidden for url: https://api.hubapi.com": "Your HubSpot credentials do not have permission to access this data. Please reconnect your HubSpot account and ensure it has the required permissions, then try again.", # Raised by source_for_pipeline when the source config carries no refresh token at all # (integration never connected or lost its token). Retrying cannot recover. diff --git a/products/warehouse_sources/backend/temporal/data_imports/sources/hubspot/test/test_source_routing.py b/products/warehouse_sources/backend/temporal/data_imports/sources/hubspot/test/test_source_routing.py index b7b749c2330c..3fe090e3e2d4 100644 --- a/products/warehouse_sources/backend/temporal/data_imports/sources/hubspot/test/test_source_routing.py +++ b/products/warehouse_sources/backend/temporal/data_imports/sources/hubspot/test/test_source_routing.py @@ -396,9 +396,13 @@ def test_source_for_pipeline_threads_resolved_version(self, pin: str | None, exp @pytest.mark.parametrize( "error_msg", [ - # Raised by fetch_data when a token refresh succeeds but the retried request is still rejected - "401 Client Error: Unauthorized for url: https://api.hubapi.com/crm/v3/properties/companies", - "401 Client Error: Unauthorized for url: https://api.hubapi.com/crm/v3/properties/deals", + # Each fetch loop refreshes the token on a 401 and re-raises this after tenacity's 5 attempts + "Hubspot API 401 - refreshed token, retrying: url=https://api.hubapi.com/crm/v3/properties/companies", + "Hubspot API 401 - refreshed token, retrying: url=https://api.hubapi.com/crm/v3/objects/deals", + "Hubspot v4 associations 401 - refreshed token, retrying: " + "url=https://api.hubapi.com/crm/v4/associations/contacts/deals/batch/read", + "Hubspot search 401 - refreshed token, retrying: url=https://api.hubapi.com/crm/v3/objects/contacts/search", + # raise_for_hubspot_status maps a 403 to this verbatim "403 Client Error: Forbidden for url: https://api.hubapi.com/crm/v3/objects/contacts", ], )