Skip to content

Commit c9af28e

Browse files
seligj95Copilot
andcommitted
[App Service] Fix #29290: Improve error message for az webapp deploy --src-url Bad Request
When `az webapp deploy --src-url` receives an HTTP 400 response with an empty body, the CLI previously displayed just "Bad Request" with no actionable guidance. This was because `send_raw_request` raises `HTTPError` before the deploy-specific error handling code is reached. This change wraps the ARM deploy request in a helper that catches 400 responses and provides a clear error message with possible causes: - Source URL not accessible or SAS token expired - URL doesn't point to a valid deployment artifact - Artifact type mismatch If the response body contains details from ARM, those are included too. Fixes #29290 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent dc9fdb3 commit c9af28e

File tree

2 files changed

+99
-5
lines changed

2 files changed

+99
-5
lines changed

src/azure-cli/azure/cli/command_modules/appservice/custom.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9781,6 +9781,28 @@ def _warmup_kudu_and_get_cookie_internal(params):
97819781
return None
97829782

97839783

9784+
def _send_deploy_request(cli_ctx, deploy_url, body):
9785+
"""Wrapper around send_raw_request for --src-url deployments that provides
9786+
actionable error messages instead of bare HTTP status codes."""
9787+
from azure.cli.core.azclierror import HTTPError
9788+
try:
9789+
return send_raw_request(cli_ctx, "PUT", deploy_url, body=body)
9790+
except HTTPError as ex:
9791+
status_code = ex.response.status_code if hasattr(ex, 'response') and ex.response is not None else None
9792+
response_text = ex.response.text if hasattr(ex, 'response') and ex.response is not None else ""
9793+
if status_code == 400:
9794+
error_detail = f" Details: {response_text}" if response_text else ""
9795+
raise CLIError(
9796+
f"Deployment from URL failed with status 400 (Bad Request).{error_detail}\n"
9797+
"Possible causes:\n"
9798+
" - The source URL is not publicly accessible or the SAS token has expired\n"
9799+
" - The URL does not point to a valid deployment artifact\n"
9800+
" - The artifact type does not match the file content (e.g., --type zip for a non-zip file)\n"
9801+
"Please verify the URL is accessible and the artifact type is correct."
9802+
) from ex
9803+
raise
9804+
9805+
97849806
def _make_onedeploy_request(params):
97859807
import requests
97869808
from azure.cli.core.util import should_disable_connection_verify
@@ -9828,16 +9850,16 @@ def _make_onedeploy_request(params):
98289850
if cookies is None:
98299851
logger.info("Failed to fetch affinity cookie for Kudu. "
98309852
"Deployment will proceed without pre-warming a Kudu instance.")
9831-
response = send_raw_request(params.cmd.cli_ctx, "PUT", deploy_url, body=body)
9853+
response = _send_deploy_request(params.cmd.cli_ctx, deploy_url, body)
98329854
else:
98339855
deploy_arm_url = _build_onedeploy_url(params, cookies.get("ARRAffinity"))
9834-
response = send_raw_request(params.cmd.cli_ctx, "PUT", deploy_arm_url, body=body)
9856+
response = _send_deploy_request(params.cmd.cli_ctx, deploy_arm_url, body)
98359857
except Exception as ex: # pylint: disable=broad-except
98369858
logger.info("Failed to deploy using instances endpoint. "
98379859
"Deployment will proceed without pre-warming a Kudu instance. Ex: %s", ex)
9838-
response = send_raw_request(params.cmd.cli_ctx, "PUT", deploy_url, body=body)
9860+
response = _send_deploy_request(params.cmd.cli_ctx, deploy_url, body)
98399861
else:
9840-
response = send_raw_request(params.cmd.cli_ctx, "PUT", deploy_url, body=body)
9862+
response = _send_deploy_request(params.cmd.cli_ctx, deploy_url, body)
98419863
poll_async_deployment_for_debugging = False
98429864

98439865
# check the status of deployment

src/azure-cli/azure/cli/command_modules/appservice/tests/latest/test_webapp_commands_thru_mock.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
add_github_actions,
3434
update_app_settings,
3535
update_application_settings_polling,
36-
update_webapp)
36+
update_webapp,
37+
_send_deploy_request)
3738

3839
# pylint: disable=line-too-long
3940
from azure.cli.core.profiles import ResourceType
@@ -639,6 +640,77 @@ def test_update_webapp_platform_release_channel_latest(self):
639640
self.assertEqual(result.additional_properties["properties"]["platformReleaseChannel"], "Latest")
640641

641642

643+
class TestSendDeployRequest(unittest.TestCase):
644+
"""Tests for _send_deploy_request wrapper that provides actionable error messages."""
645+
646+
@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request')
647+
def test_success_passes_through(self, send_raw_request_mock):
648+
"""Successful responses (200/202) should pass through unchanged."""
649+
cli_ctx = _get_test_cmd().cli_ctx
650+
response = mock.MagicMock()
651+
response.status_code = 202
652+
send_raw_request_mock.return_value = response
653+
654+
result = _send_deploy_request(cli_ctx, 'https://management.azure.com/deploy', '{"properties":{}}')
655+
656+
self.assertEqual(result, response)
657+
send_raw_request_mock.assert_called_once_with(
658+
cli_ctx, "PUT", 'https://management.azure.com/deploy', body='{"properties":{}}'
659+
)
660+
661+
@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request')
662+
def test_400_with_empty_body_gives_actionable_error(self, send_raw_request_mock):
663+
"""HTTP 400 with empty body should produce a helpful error message instead of bare 'Bad Request'."""
664+
from azure.cli.core.azclierror import HTTPError
665+
cli_ctx = _get_test_cmd().cli_ctx
666+
667+
response = mock.MagicMock()
668+
response.status_code = 400
669+
response.text = ""
670+
send_raw_request_mock.side_effect = HTTPError("Bad Request", response)
671+
672+
with self.assertRaises(CLIError) as ctx:
673+
_send_deploy_request(cli_ctx, 'https://management.azure.com/deploy', '{"properties":{}}')
674+
675+
error_msg = str(ctx.exception)
676+
self.assertIn("Deployment from URL failed with status 400", error_msg)
677+
self.assertIn("source URL is not publicly accessible", error_msg)
678+
self.assertIn("SAS token has expired", error_msg)
679+
self.assertIn("artifact type does not match", error_msg)
680+
681+
@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request')
682+
def test_400_with_response_body_includes_details(self, send_raw_request_mock):
683+
"""HTTP 400 with a response body should include the details in the error."""
684+
from azure.cli.core.azclierror import HTTPError
685+
cli_ctx = _get_test_cmd().cli_ctx
686+
687+
response = mock.MagicMock()
688+
response.status_code = 400
689+
response.text = "Invalid package URI"
690+
send_raw_request_mock.side_effect = HTTPError("Bad Request(Invalid package URI)", response)
691+
692+
with self.assertRaises(CLIError) as ctx:
693+
_send_deploy_request(cli_ctx, 'https://management.azure.com/deploy', '{"properties":{}}')
694+
695+
error_msg = str(ctx.exception)
696+
self.assertIn("Deployment from URL failed with status 400", error_msg)
697+
self.assertIn("Invalid package URI", error_msg)
698+
699+
@mock.patch('azure.cli.command_modules.appservice.custom.send_raw_request')
700+
def test_non_400_error_reraises(self, send_raw_request_mock):
701+
"""Non-400 HTTP errors should re-raise without modification."""
702+
from azure.cli.core.azclierror import HTTPError
703+
cli_ctx = _get_test_cmd().cli_ctx
704+
705+
response = mock.MagicMock()
706+
response.status_code = 500
707+
response.text = "Internal Server Error"
708+
send_raw_request_mock.side_effect = HTTPError("Internal Server Error", response)
709+
710+
with self.assertRaises(HTTPError):
711+
_send_deploy_request(cli_ctx, 'https://management.azure.com/deploy', '{"properties":{}}')
712+
713+
642714
class FakedResponse: # pylint: disable=too-few-public-methods
643715
def __init__(self, status_code):
644716
self.status_code = status_code

0 commit comments

Comments
 (0)