From 30c58f052d328675bc20974080fff5cc370aa29d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 04:38:32 +0000 Subject: [PATCH 1/4] Initial plan From eeb971c63bf6e0e3cc48672aeba834686d0769df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 04:46:13 +0000 Subject: [PATCH 2/4] {Core} Handle 403/DecodeError when listing tenants in az login --- src/azure-cli-core/azure/cli/core/_profile.py | 26 ++++++++- .../azure/cli/core/tests/test_profile.py | 56 +++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/azure-cli-core/azure/cli/core/_profile.py b/src/azure-cli-core/azure/cli/core/_profile.py index 13f47ed417c..c6988bff284 100644 --- a/src/azure-cli-core/azure/cli/core/_profile.py +++ b/src/azure-cli-core/azure/cli/core/_profile.py @@ -826,7 +826,31 @@ def find_using_common_tenant(self, username, credential=None): client = self._create_subscription_client(credential) # https://learn.microsoft.com/en-us/rest/api/resources/tenants/list - tenants = client.tenants.list() + try: + tenants = list(client.tenants.list()) + except Exception as ex: # pylint: disable=broad-except + from azure.core.exceptions import DecodeError, HttpResponseError + from azure.cli.core.azclierror import AzureResponseError + if isinstance(ex, DecodeError): + raise AzureResponseError( + "Failed to retrieve tenants. The response from the server could not be parsed. " + "This may be caused by a network firewall or proxy returning an unexpected response. " + "Please check your network settings and try again, or use " + "'az login --tenant TENANT_ID' to log in to a specific tenant." + ) from ex + if isinstance(ex, HttpResponseError): + status_code = ex.status_code + if status_code == 403: + raise AzureResponseError( + "Failed to retrieve tenants. The request was blocked (HTTP 403 Forbidden). " + "This may be caused by a network firewall, proxy, or Conditional Access policy. " + "Please check your network settings and try again, or use " + "'az login --tenant TENANT_ID' to log in to a specific tenant." + ) from ex + raise AzureResponseError( + "Failed to retrieve tenants (HTTP {}): {}".format(status_code, ex) + ) from ex + raise for t in tenants: tenant_id = t.tenant_id diff --git a/src/azure-cli-core/azure/cli/core/tests/test_profile.py b/src/azure-cli-core/azure/cli/core/tests/test_profile.py index d62464ca5d3..38cd20baee9 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_profile.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_profile.py @@ -684,6 +684,62 @@ def test_login_no_subscription_raises_error(self, can_launch_browser_mock, profile.login(True, None, None, False, None, use_device_code=False, allow_no_subscriptions=False) + @mock.patch('azure.cli.core._profile.SubscriptionFinder._create_subscription_client', autospec=True) + @mock.patch('azure.cli.core.auth.identity.Identity.get_user_credential', autospec=True) + @mock.patch('azure.cli.core.auth.identity.Identity.login_with_auth_code', autospec=True) + @mock.patch('azure.cli.core._profile.can_launch_browser', autospec=True, return_value=True) + def test_login_tenant_list_403_raises_friendly_error(self, can_launch_browser_mock, + login_with_auth_code_mock, get_user_credential_mock, + create_subscription_client_mock): + """When listing tenants returns 403 (e.g. network block), a friendly AzureResponseError is raised.""" + from azure.core.exceptions import HttpResponseError + from azure.cli.core.azclierror import AzureResponseError + login_with_auth_code_mock.return_value = self.user_identity_mock + + cli = DummyCli() + mock_subscription_client = mock.MagicMock() + http_response_mock = mock.MagicMock() + http_response_mock.status_code = 403 + http_error = HttpResponseError(response=http_response_mock) + mock_subscription_client.tenants.list.side_effect = http_error + create_subscription_client_mock.return_value = mock_subscription_client + + storage_mock = {'subscriptions': None} + profile = Profile(cli_ctx=cli, storage=storage_mock) + + with self.assertRaises(AzureResponseError) as cm: + profile.login(True, None, None, False, None, use_device_code=False, allow_no_subscriptions=False) + self.assertIn("403", str(cm.exception)) + + @mock.patch('azure.cli.core._profile.SubscriptionFinder._create_subscription_client', autospec=True) + @mock.patch('azure.cli.core.auth.identity.Identity.get_user_credential', autospec=True) + @mock.patch('azure.cli.core.auth.identity.Identity.login_with_auth_code', autospec=True) + @mock.patch('azure.cli.core._profile.can_launch_browser', autospec=True, return_value=True) + def test_login_tenant_list_decode_error_raises_friendly_error(self, can_launch_browser_mock, + login_with_auth_code_mock, + get_user_credential_mock, + create_subscription_client_mock): + """When listing tenants returns a non-JSON response (e.g. HTML 403 block page), a friendly error is raised.""" + from azure.core.exceptions import DecodeError + from azure.cli.core.azclierror import AzureResponseError + login_with_auth_code_mock.return_value = self.user_identity_mock + + cli = DummyCli() + mock_subscription_client = mock.MagicMock() + mock_subscription_client.tenants.list.side_effect = DecodeError( + message="JSON is invalid: Expecting value: line 1 column 1 (char 0)", + response=mock.MagicMock(), + error=None + ) + create_subscription_client_mock.return_value = mock_subscription_client + + storage_mock = {'subscriptions': None} + profile = Profile(cli_ctx=cli, storage=storage_mock) + + with self.assertRaises(AzureResponseError) as cm: + profile.login(True, None, None, False, None, use_device_code=False, allow_no_subscriptions=False) + self.assertIn("could not be parsed", str(cm.exception)) + @mock.patch('azure.cli.core._profile.SubscriptionFinder._create_subscription_client', autospec=True) @mock.patch('azure.cli.core.auth.identity.Identity.get_user_credential', autospec=True) @mock.patch('azure.cli.core.auth.identity.Identity.login_with_auth_code', autospec=True) From c56fb33d1331d0ae2889a39467032d01c8c79125 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 04:47:35 +0000 Subject: [PATCH 3/4] {Core} Refine exception handling for tenant list to use specific types --- src/azure-cli-core/azure/cli/core/_profile.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_profile.py b/src/azure-cli-core/azure/cli/core/_profile.py index c6988bff284..ff18b453fcc 100644 --- a/src/azure-cli-core/azure/cli/core/_profile.py +++ b/src/azure-cli-core/azure/cli/core/_profile.py @@ -826,11 +826,11 @@ def find_using_common_tenant(self, username, credential=None): client = self._create_subscription_client(credential) # https://learn.microsoft.com/en-us/rest/api/resources/tenants/list + from azure.core.exceptions import DecodeError, HttpResponseError + from azure.cli.core.azclierror import AzureResponseError try: tenants = list(client.tenants.list()) - except Exception as ex: # pylint: disable=broad-except - from azure.core.exceptions import DecodeError, HttpResponseError - from azure.cli.core.azclierror import AzureResponseError + except (DecodeError, HttpResponseError) as ex: if isinstance(ex, DecodeError): raise AzureResponseError( "Failed to retrieve tenants. The response from the server could not be parsed. " @@ -838,19 +838,18 @@ def find_using_common_tenant(self, username, credential=None): "Please check your network settings and try again, or use " "'az login --tenant TENANT_ID' to log in to a specific tenant." ) from ex - if isinstance(ex, HttpResponseError): - status_code = ex.status_code - if status_code == 403: - raise AzureResponseError( - "Failed to retrieve tenants. The request was blocked (HTTP 403 Forbidden). " - "This may be caused by a network firewall, proxy, or Conditional Access policy. " - "Please check your network settings and try again, or use " - "'az login --tenant TENANT_ID' to log in to a specific tenant." - ) from ex + # HttpResponseError (but not DecodeError) + status_code = ex.status_code + if status_code == 403: raise AzureResponseError( - "Failed to retrieve tenants (HTTP {}): {}".format(status_code, ex) + "Failed to retrieve tenants. The request was blocked (HTTP 403 Forbidden). " + "This may be caused by a network firewall, proxy, or Conditional Access policy. " + "Please check your network settings and try again, or use " + "'az login --tenant TENANT_ID' to log in to a specific tenant." ) from ex - raise + raise AzureResponseError( + "Failed to retrieve tenants (HTTP {}): {}".format(status_code, ex) + ) from ex for t in tenants: tenant_id = t.tenant_id From efec20be4638e36f8bf4559d2c52c3d057d3864d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 04:48:45 +0000 Subject: [PATCH 4/4] {Core} Re-raise non-403 HttpResponseError with original context --- src/azure-cli-core/azure/cli/core/_profile.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/azure-cli-core/azure/cli/core/_profile.py b/src/azure-cli-core/azure/cli/core/_profile.py index ff18b453fcc..dee887e741f 100644 --- a/src/azure-cli-core/azure/cli/core/_profile.py +++ b/src/azure-cli-core/azure/cli/core/_profile.py @@ -829,6 +829,9 @@ def find_using_common_tenant(self, username, credential=None): from azure.core.exceptions import DecodeError, HttpResponseError from azure.cli.core.azclierror import AzureResponseError try: + # Eagerly materialise tenants here so any HTTP errors (e.g. firewall blocking the + # request with a non-JSON response) are caught below before iteration begins. + # Tenant counts are typically small so materialising up-front is not a concern. tenants = list(client.tenants.list()) except (DecodeError, HttpResponseError) as ex: if isinstance(ex, DecodeError): @@ -839,17 +842,14 @@ def find_using_common_tenant(self, username, credential=None): "'az login --tenant TENANT_ID' to log in to a specific tenant." ) from ex # HttpResponseError (but not DecodeError) - status_code = ex.status_code - if status_code == 403: + if ex.status_code == 403: raise AzureResponseError( "Failed to retrieve tenants. The request was blocked (HTTP 403 Forbidden). " "This may be caused by a network firewall, proxy, or Conditional Access policy. " "Please check your network settings and try again, or use " "'az login --tenant TENANT_ID' to log in to a specific tenant." ) from ex - raise AzureResponseError( - "Failed to retrieve tenants (HTTP {}): {}".format(status_code, ex) - ) from ex + raise for t in tenants: tenant_id = t.tenant_id