From 763afc7931a4b5889a94dfd2bd41fb440daf4378 Mon Sep 17 00:00:00 2001 From: James Chapman Date: Tue, 9 Jun 2026 09:27:06 +0000 Subject: [PATCH 1/9] Add test for handling invalid JSON in receive_message function and provide fix for the infinite looped service bus issue --- CHANGELOG.md | 3 +++ resource_processor/_version.py | 2 +- resource_processor/tests_rp/test_runner.py | 26 ++++++++++++++++++++++ resource_processor/vmss_porter/runner.py | 2 ++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f99328b247..e86a695ca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ ENHANCEMENTS: * Pin all GitHub Actions workflow steps to full commit SHAs to prevent supply chain attacks plus update to latest releases ([#4886](https://github.com/microsoft/AzureTRE/pull/4886)) * Add Windows Server 2025 image support to Guacamole. ([#4890](https://github.com/microsoft/AzureTRE/issues/4890)) +BUG FIXES: +* Fix to prevent infinite retry loops in service bus + ## (0.28.0) (March 2, 2026) **BREAKING CHANGES** * Sonatype Nexus shared service now requires explicit EULA acceptance (`accept_nexus_eula: true`) when deploying. This ensures compliance with Sonatype Nexus Community Edition licensing. ([#4842](https://github.com/microsoft/AzureTRE/issues/4842)) diff --git a/resource_processor/_version.py b/resource_processor/_version.py index 26c36ca79a..f2b93b13de 100644 --- a/resource_processor/_version.py +++ b/resource_processor/_version.py @@ -1 +1 @@ -__version__ = "0.13.3" +__version__ = "0.13.4" diff --git a/resource_processor/tests_rp/test_runner.py b/resource_processor/tests_rp/test_runner.py index 6c9166b017..2a4e526987 100644 --- a/resource_processor/tests_rp/test_runner.py +++ b/resource_processor/tests_rp/test_runner.py @@ -128,6 +128,32 @@ async def test_receive_message(mock_invoke_porter_action, mock_service_bus_clien mock_service_bus_client_instance.get_queue_receiver.assert_called_once_with(queue_name="test_queue", max_wait_time=1, session_id=ServiceBusSessionFilter.NEXT_AVAILABLE) +@pytest.mark.asyncio +async def test_receive_message_bad_json(mock_service_bus_client, mock_auto_lock_renewer): + mock_service_bus_client_instance = mock_service_bus_client.return_value + + # Set up the lock renewer mock correctly + mock_renewer = AsyncMock() + mock_renewer.register = Mock() + mock_auto_lock_renewer.return_value.__aenter__.return_value = mock_renewer + + mock_receiver = AsyncMock() + mock_receiver.__aenter__.return_value = mock_receiver + mock_receiver.__aexit__.return_value = None + mock_receiver.session.session_id = "test_session_id" + mock_receiver.__aiter__.return_value = ["invalid_json_string"] + + mock_service_bus_client_instance.get_queue_receiver.return_value.__aenter__.return_value = mock_receiver + + run_once = Mock(side_effect=[True, False]) + + config = {"resource_request_queue": "test_queue"} + + await receive_message(mock_service_bus_client_instance, config, keep_running=run_once) + mock_receiver.dead_letter_message.assert_called_once_with("invalid_json_string", reason="InvalidJSON") + mock_receiver.complete_message.assert_not_called() + + @pytest.mark.asyncio async def test_receive_message_unknown_exception(mock_auto_lock_renewer, mock_service_bus_client, mock_logger): """Test receiving a message with an unknown exception.""" diff --git a/resource_processor/vmss_porter/runner.py b/resource_processor/vmss_porter/runner.py index 120ececba0..a72b82e3c9 100644 --- a/resource_processor/vmss_porter/runner.py +++ b/resource_processor/vmss_porter/runner.py @@ -73,6 +73,8 @@ async def receive_message(service_bus_client, config: dict, keep_running=lambda: message = json.loads(str(msg)) except (json.JSONDecodeError) as e: logger.error(f"Received bad service bus resource request message: {e}") + await receiver.dead_letter_message(msg, reason="InvalidJSON") + continue with tracer.start_as_current_span("receive_message") as current_span: current_span.set_attribute("resource_id", message["id"]) From 5bceb29b2d15775c83bb85f551e85bfcbd08b841 Mon Sep 17 00:00:00 2001 From: James Chapman Date: Tue, 9 Jun 2026 09:29:52 +0000 Subject: [PATCH 2/9] update changelog message --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e86a695ca5..7c9321ecdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ ENHANCEMENTS: * Add Windows Server 2025 image support to Guacamole. ([#4890](https://github.com/microsoft/AzureTRE/issues/4890)) BUG FIXES: -* Fix to prevent infinite retry loops in service bus +* Fix to enhance service bus handling of invalid JSON in receive_message function ## (0.28.0) (March 2, 2026) **BREAKING CHANGES** From 96d1b91f809f338e52868dd6fe3315c9c6dc6253 Mon Sep 17 00:00:00 2001 From: James Chapman <196318169+JC-wk@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:37:40 +0100 Subject: [PATCH 3/9] add error_description Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- resource_processor/vmss_porter/runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_processor/vmss_porter/runner.py b/resource_processor/vmss_porter/runner.py index a72b82e3c9..7d91b45c3c 100644 --- a/resource_processor/vmss_porter/runner.py +++ b/resource_processor/vmss_porter/runner.py @@ -73,7 +73,7 @@ async def receive_message(service_bus_client, config: dict, keep_running=lambda: message = json.loads(str(msg)) except (json.JSONDecodeError) as e: logger.error(f"Received bad service bus resource request message: {e}") - await receiver.dead_letter_message(msg, reason="InvalidJSON") + await receiver.dead_letter_message(msg, reason="InvalidJSON", error_description=str(e)) continue with tracer.start_as_current_span("receive_message") as current_span: From c760652232d624bd9af58762d0981fb0cd25bbcd Mon Sep 17 00:00:00 2001 From: James Chapman Date: Tue, 9 Jun 2026 09:40:57 +0000 Subject: [PATCH 4/9] Update changelog and improve test assertions for JSON handling in receive_message function --- CHANGELOG.md | 2 +- resource_processor/tests_rp/test_runner.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c9321ecdb..6d328bf988 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ ENHANCEMENTS: * Add Windows Server 2025 image support to Guacamole. ([#4890](https://github.com/microsoft/AzureTRE/issues/4890)) BUG FIXES: -* Fix to enhance service bus handling of invalid JSON in receive_message function +* Fix to enhance service bus handling of invalid JSON in receive_message function ([#4932](https://github.com/microsoft/AzureTRE/pull/4932)) ## (0.28.0) (March 2, 2026) **BREAKING CHANGES** diff --git a/resource_processor/tests_rp/test_runner.py b/resource_processor/tests_rp/test_runner.py index 2a4e526987..e3ba98f949 100644 --- a/resource_processor/tests_rp/test_runner.py +++ b/resource_processor/tests_rp/test_runner.py @@ -124,7 +124,7 @@ async def test_receive_message(mock_invoke_porter_action, mock_service_bus_clien config = {"resource_request_queue": "test_queue"} await receive_message(mock_service_bus_client_instance, config, keep_running=run_once) - mock_receiver.complete_message.assert_called_once() + mock_receiver.complete_message.assert_awaited_once() mock_service_bus_client_instance.get_queue_receiver.assert_called_once_with(queue_name="test_queue", max_wait_time=1, session_id=ServiceBusSessionFilter.NEXT_AVAILABLE) @@ -150,8 +150,12 @@ async def test_receive_message_bad_json(mock_service_bus_client, mock_auto_lock_ config = {"resource_request_queue": "test_queue"} await receive_message(mock_service_bus_client_instance, config, keep_running=run_once) - mock_receiver.dead_letter_message.assert_called_once_with("invalid_json_string", reason="InvalidJSON") - mock_receiver.complete_message.assert_not_called() + mock_receiver.dead_letter_message.assert_awaited_once_with( + "invalid_json_string", + reason="InvalidJSON", + error_description="Expecting value: line 1 column 1 (char 0)" + ) + mock_receiver.complete_message.assert_not_awaited() @pytest.mark.asyncio From 7074322047ee34d7f115646149ccaacd2a592953 Mon Sep 17 00:00:00 2001 From: James Chapman Date: Mon, 10 Aug 2026 12:18:32 +0000 Subject: [PATCH 5/9] copilot fix --- resource_processor/tests_rp/test_runner.py | 43 ++++++++++++++++++++++ resource_processor/vmss_porter/runner.py | 11 +++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/resource_processor/tests_rp/test_runner.py b/resource_processor/tests_rp/test_runner.py index e3ba98f949..b8a3fb181f 100644 --- a/resource_processor/tests_rp/test_runner.py +++ b/resource_processor/tests_rp/test_runner.py @@ -158,6 +158,49 @@ async def test_receive_message_bad_json(mock_service_bus_client, mock_auto_lock_ mock_receiver.complete_message.assert_not_awaited() +@pytest.mark.asyncio +@pytest.mark.parametrize( + "message, error_description", + [ + ("null", "Resource request message must be a JSON object"), + ("[]", "Resource request message must be a JSON object"), + ("\"text\"", "Resource request message must be a JSON object"), + ( + '{"id": "test_id"}', + "Resource request message is missing fields: ['action', 'operationId', 'stepId']", + ), + ], +) +async def test_receive_message_invalid_json_structure( + message, error_description, mock_service_bus_client, mock_auto_lock_renewer +): + mock_service_bus_client_instance = mock_service_bus_client.return_value + + mock_renewer = AsyncMock() + mock_renewer.register = Mock() + mock_auto_lock_renewer.return_value.__aenter__.return_value = mock_renewer + + mock_receiver = AsyncMock() + mock_receiver.__aenter__.return_value = mock_receiver + mock_receiver.__aexit__.return_value = None + mock_receiver.session.session_id = "test_session_id" + mock_receiver.__aiter__.return_value = [message] + + mock_service_bus_client_instance.get_queue_receiver.return_value.__aenter__.return_value = mock_receiver + + run_once = Mock(side_effect=[True, False]) + config = {"resource_request_queue": "test_queue"} + + await receive_message(mock_service_bus_client_instance, config, keep_running=run_once) + + mock_receiver.dead_letter_message.assert_awaited_once_with( + message, + reason="InvalidJSON", + error_description=error_description, + ) + mock_receiver.complete_message.assert_not_awaited() + + @pytest.mark.asyncio async def test_receive_message_unknown_exception(mock_auto_lock_renewer, mock_service_bus_client, mock_logger): """Test receiving a message with an unknown exception.""" diff --git a/resource_processor/vmss_porter/runner.py b/resource_processor/vmss_porter/runner.py index 7d91b45c3c..13175e0b0c 100644 --- a/resource_processor/vmss_porter/runner.py +++ b/resource_processor/vmss_porter/runner.py @@ -18,6 +18,9 @@ from azure.identity.aio import DefaultAzureCredential +REQUIRED_RESOURCE_REQUEST_FIELDS = {"id", "action", "stepId", "operationId"} + + def set_up_config() -> Optional[dict]: try: config = get_config() @@ -71,7 +74,13 @@ async def receive_message(service_bus_client, config: dict, keep_running=lambda: try: message = json.loads(str(msg)) - except (json.JSONDecodeError) as e: + if not isinstance(message, dict): + raise ValueError("Resource request message must be a JSON object") + + missing_fields = REQUIRED_RESOURCE_REQUEST_FIELDS - message.keys() + if missing_fields: + raise ValueError(f"Resource request message is missing fields: {sorted(missing_fields)}") + except (json.JSONDecodeError, ValueError) as e: logger.error(f"Received bad service bus resource request message: {e}") await receiver.dead_letter_message(msg, reason="InvalidJSON", error_description=str(e)) continue From d0bd1fd970bcbc2947f787fe4b58f0101723be57 Mon Sep 17 00:00:00 2001 From: James Chapman Date: Mon, 10 Aug 2026 16:11:45 +0000 Subject: [PATCH 6/9] Implemented complete resource-request validation in runner.py. It now validates required fields and types for id, action, stepId, operationId, name, version, and parameters, plus the optional user object, before any status is sent or processing begins. Invalid messages are dead-lettered as InvalidJSON. Added regression coverage in test_runner.py. Focused suite passes: 27 tests. --- resource_processor/tests_rp/test_runner.py | 34 +++++++++++++++-- resource_processor/vmss_porter/runner.py | 43 ++++++++++++++++++---- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/resource_processor/tests_rp/test_runner.py b/resource_processor/tests_rp/test_runner.py index b8a3fb181f..b3cdf15870 100644 --- a/resource_processor/tests_rp/test_runner.py +++ b/resource_processor/tests_rp/test_runner.py @@ -115,7 +115,15 @@ async def test_receive_message(mock_invoke_porter_action, mock_service_bus_clien mock_receiver.__aexit__.return_value = None mock_receiver.session.session_id = "test_session_id" mock_receiver.__aiter__.return_value = [AsyncMock()] - mock_receiver.__aiter__.return_value[0] = json.dumps({"id": "test_id", "action": "install", "stepId": "test_step_id", "operationId": "test_operation_id"}) + mock_receiver.__aiter__.return_value[0] = json.dumps({ + "id": "test_id", + "action": "install", + "stepId": "test_step_id", + "operationId": "test_operation_id", + "name": "test_bundle", + "version": "1.0.0", + "parameters": {}, + }) mock_service_bus_client_instance.get_queue_receiver.return_value.__aenter__.return_value = mock_receiver @@ -167,7 +175,19 @@ async def test_receive_message_bad_json(mock_service_bus_client, mock_auto_lock_ ("\"text\"", "Resource request message must be a JSON object"), ( '{"id": "test_id"}', - "Resource request message is missing fields: ['action', 'operationId', 'stepId']", + "Resource request message is missing fields: ['action', 'name', 'operationId', 'parameters', 'stepId', 'version']", + ), + ( + '{"id": "test_id", "action": [], "stepId": "test_step_id", "operationId": "test_operation_id", "name": "test_bundle", "version": "1.0.0", "parameters": {}}', + "Resource request message has invalid field types: ['action']", + ), + ( + '{"id": "test_id", "action": "install", "stepId": "test_step_id", "operationId": "test_operation_id", "name": "test_bundle", "version": "1.0.0", "parameters": null}', + "Resource request message has invalid field types: ['parameters']", + ), + ( + '{"id": "test_id", "action": "install", "stepId": "test_step_id", "operationId": "test_operation_id", "name": "test_bundle", "version": "1.0.0", "parameters": {}, "user": []}', + "Resource request message has invalid field types: ['user']", ), ], ) @@ -216,7 +236,15 @@ async def test_receive_message_unknown_exception(mock_auto_lock_renewer, mock_se mock_receiver.__aexit__.return_value = None mock_receiver.session.session_id = "test_session_id" mock_receiver.__aiter__.return_value = [AsyncMock()] - mock_receiver.__aiter__.return_value[0] = json.dumps({"id": "test_id", "action": "install", "stepId": "test_step_id", "operationId": "test_operation_id"}) + mock_receiver.__aiter__.return_value[0] = json.dumps({ + "id": "test_id", + "action": "install", + "stepId": "test_step_id", + "operationId": "test_operation_id", + "name": "test_bundle", + "version": "1.0.0", + "parameters": {}, + }) mock_service_bus_client_instance.get_queue_receiver.return_value.__aenter__.return_value = mock_receiver diff --git a/resource_processor/vmss_porter/runner.py b/resource_processor/vmss_porter/runner.py index 13175e0b0c..ac6a753a3d 100644 --- a/resource_processor/vmss_porter/runner.py +++ b/resource_processor/vmss_porter/runner.py @@ -18,7 +18,41 @@ from azure.identity.aio import DefaultAzureCredential -REQUIRED_RESOURCE_REQUEST_FIELDS = {"id", "action", "stepId", "operationId"} +RESOURCE_REQUEST_FIELD_TYPES = { + "id": str, + "action": str, + "stepId": str, + "operationId": str, + "name": str, + "version": str, + "parameters": dict, +} +OPTIONAL_RESOURCE_REQUEST_FIELD_TYPES = {"user": dict} + + +def validate_resource_request(message: object) -> None: + if not isinstance(message, dict): + raise ValueError("Resource request message must be a JSON object") + + missing_fields = set(RESOURCE_REQUEST_FIELD_TYPES) - message.keys() + if missing_fields: + raise ValueError(f"Resource request message is missing fields: {sorted(missing_fields)}") + + invalid_fields = [ + field_name + for field_name, field_type in RESOURCE_REQUEST_FIELD_TYPES.items() + if not isinstance(message[field_name], field_type) + ] + if invalid_fields: + raise ValueError(f"Resource request message has invalid field types: {sorted(invalid_fields)}") + + invalid_optional_fields = [ + field_name + for field_name, field_type in OPTIONAL_RESOURCE_REQUEST_FIELD_TYPES.items() + if field_name in message and not isinstance(message[field_name], field_type) + ] + if invalid_optional_fields: + raise ValueError(f"Resource request message has invalid field types: {sorted(invalid_optional_fields)}") def set_up_config() -> Optional[dict]: @@ -74,12 +108,7 @@ async def receive_message(service_bus_client, config: dict, keep_running=lambda: try: message = json.loads(str(msg)) - if not isinstance(message, dict): - raise ValueError("Resource request message must be a JSON object") - - missing_fields = REQUIRED_RESOURCE_REQUEST_FIELDS - message.keys() - if missing_fields: - raise ValueError(f"Resource request message is missing fields: {sorted(missing_fields)}") + validate_resource_request(message) except (json.JSONDecodeError, ValueError) as e: logger.error(f"Received bad service bus resource request message: {e}") await receiver.dead_letter_message(msg, reason="InvalidJSON", error_description=str(e)) From 978e7938767d8569bce98b89bd4ac7bf3c42185a Mon Sep 17 00:00:00 2001 From: James Chapman Date: Mon, 10 Aug 2026 16:41:52 +0000 Subject: [PATCH 7/9] address PR feedback --- resource_processor/tests_rp/test_runner.py | 36 +++++++++++++++++++--- resource_processor/vmss_porter/runner.py | 5 ++- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/resource_processor/tests_rp/test_runner.py b/resource_processor/tests_rp/test_runner.py index b3cdf15870..ad13f6f291 100644 --- a/resource_processor/tests_rp/test_runner.py +++ b/resource_processor/tests_rp/test_runner.py @@ -158,11 +158,37 @@ async def test_receive_message_bad_json(mock_service_bus_client, mock_auto_lock_ config = {"resource_request_queue": "test_queue"} await receive_message(mock_service_bus_client_instance, config, keep_running=run_once) - mock_receiver.dead_letter_message.assert_awaited_once_with( - "invalid_json_string", - reason="InvalidJSON", - error_description="Expecting value: line 1 column 1 (char 0)" - ) + mock_receiver.dead_letter_message.assert_awaited_once() + dead_letter_args, dead_letter_kwargs = mock_receiver.dead_letter_message.await_args + assert dead_letter_args == ("invalid_json_string",) + assert dead_letter_kwargs["reason"] == "InvalidJSON" + assert dead_letter_kwargs["error_description"] + mock_receiver.complete_message.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_receive_message_dead_letter_failure_is_logged(mock_service_bus_client, mock_auto_lock_renewer, mock_logger): + mock_service_bus_client_instance = mock_service_bus_client.return_value + + mock_renewer = AsyncMock() + mock_renewer.register = Mock() + mock_auto_lock_renewer.return_value.__aenter__.return_value = mock_renewer + + mock_receiver = AsyncMock() + mock_receiver.__aenter__.return_value = mock_receiver + mock_receiver.__aexit__.return_value = None + mock_receiver.session.session_id = "test_session_id" + mock_receiver.__aiter__.return_value = ["invalid_json_string"] + mock_receiver.dead_letter_message.side_effect = RuntimeError("lock lost") + + mock_service_bus_client_instance.get_queue_receiver.return_value.__aenter__.return_value = mock_receiver + + run_once = Mock(side_effect=[True, False]) + config = {"resource_request_queue": "test_queue"} + + await receive_message(mock_service_bus_client_instance, config, keep_running=run_once) + + mock_logger.exception.assert_called_once_with("Failed to dead-letter malformed message") mock_receiver.complete_message.assert_not_awaited() diff --git a/resource_processor/vmss_porter/runner.py b/resource_processor/vmss_porter/runner.py index ac6a753a3d..70a81cc9af 100644 --- a/resource_processor/vmss_porter/runner.py +++ b/resource_processor/vmss_porter/runner.py @@ -111,7 +111,10 @@ async def receive_message(service_bus_client, config: dict, keep_running=lambda: validate_resource_request(message) except (json.JSONDecodeError, ValueError) as e: logger.error(f"Received bad service bus resource request message: {e}") - await receiver.dead_letter_message(msg, reason="InvalidJSON", error_description=str(e)) + try: + await receiver.dead_letter_message(msg, reason="InvalidJSON", error_description=str(e)) + except Exception: + logger.exception("Failed to dead-letter malformed message") continue with tracer.start_as_current_span("receive_message") as current_span: From f1286b342b702a63ea3e6ee741262c3b344a86d6 Mon Sep 17 00:00:00 2001 From: James Chapman Date: Mon, 17 Aug 2026 13:36:56 +0000 Subject: [PATCH 8/9] Handle invalid resource request JSON by separating decode vs schema validation errors --- resource_processor/tests_rp/test_runner.py | 17 +++++++++++++---- resource_processor/vmss_porter/runner.py | 13 +++++++++++-- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/resource_processor/tests_rp/test_runner.py b/resource_processor/tests_rp/test_runner.py index ad13f6f291..ae667de312 100644 --- a/resource_processor/tests_rp/test_runner.py +++ b/resource_processor/tests_rp/test_runner.py @@ -167,7 +167,16 @@ async def test_receive_message_bad_json(mock_service_bus_client, mock_auto_lock_ @pytest.mark.asyncio -async def test_receive_message_dead_letter_failure_is_logged(mock_service_bus_client, mock_auto_lock_renewer, mock_logger): +@pytest.mark.parametrize( + "message, expected_log", + [ + ("invalid_json_string", "Failed to dead-letter malformed message"), + ("null", "Failed to dead-letter invalid resource request message"), + ], +) +async def test_receive_message_dead_letter_failure_is_logged( + message, expected_log, mock_service_bus_client, mock_auto_lock_renewer, mock_logger +): mock_service_bus_client_instance = mock_service_bus_client.return_value mock_renewer = AsyncMock() @@ -178,7 +187,7 @@ async def test_receive_message_dead_letter_failure_is_logged(mock_service_bus_cl mock_receiver.__aenter__.return_value = mock_receiver mock_receiver.__aexit__.return_value = None mock_receiver.session.session_id = "test_session_id" - mock_receiver.__aiter__.return_value = ["invalid_json_string"] + mock_receiver.__aiter__.return_value = [message] mock_receiver.dead_letter_message.side_effect = RuntimeError("lock lost") mock_service_bus_client_instance.get_queue_receiver.return_value.__aenter__.return_value = mock_receiver @@ -188,7 +197,7 @@ async def test_receive_message_dead_letter_failure_is_logged(mock_service_bus_cl await receive_message(mock_service_bus_client_instance, config, keep_running=run_once) - mock_logger.exception.assert_called_once_with("Failed to dead-letter malformed message") + mock_logger.exception.assert_called_once_with(expected_log) mock_receiver.complete_message.assert_not_awaited() @@ -241,7 +250,7 @@ async def test_receive_message_invalid_json_structure( mock_receiver.dead_letter_message.assert_awaited_once_with( message, - reason="InvalidJSON", + reason="InvalidResourceRequest", error_description=error_description, ) mock_receiver.complete_message.assert_not_awaited() diff --git a/resource_processor/vmss_porter/runner.py b/resource_processor/vmss_porter/runner.py index 70a81cc9af..2b1eb45eef 100644 --- a/resource_processor/vmss_porter/runner.py +++ b/resource_processor/vmss_porter/runner.py @@ -108,8 +108,7 @@ async def receive_message(service_bus_client, config: dict, keep_running=lambda: try: message = json.loads(str(msg)) - validate_resource_request(message) - except (json.JSONDecodeError, ValueError) as e: + except json.JSONDecodeError as e: logger.error(f"Received bad service bus resource request message: {e}") try: await receiver.dead_letter_message(msg, reason="InvalidJSON", error_description=str(e)) @@ -117,6 +116,16 @@ async def receive_message(service_bus_client, config: dict, keep_running=lambda: logger.exception("Failed to dead-letter malformed message") continue + try: + validate_resource_request(message) + except ValueError as e: + logger.error(f"Received invalid service bus resource request message: {e}") + try: + await receiver.dead_letter_message(msg, reason="InvalidResourceRequest", error_description=str(e)) + except Exception: + logger.exception("Failed to dead-letter invalid resource request message") + continue + with tracer.start_as_current_span("receive_message") as current_span: current_span.set_attribute("resource_id", message["id"]) current_span.set_attribute("action", message["action"]) From 7a95e15ec1d8a8b3f8c394f8283f675b55cc9225 Mon Sep 17 00:00:00 2001 From: James Chapman Date: Mon, 17 Aug 2026 13:37:46 +0000 Subject: [PATCH 9/9] bump version --- resource_processor/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resource_processor/_version.py b/resource_processor/_version.py index e318db3960..af935ca6d9 100644 --- a/resource_processor/_version.py +++ b/resource_processor/_version.py @@ -1 +1 @@ -__version__ = "0.13.6" +__version__ = "0.13.7"