From 028d9531b21399ac3ae7148fe46e53073f1c7b07 Mon Sep 17 00:00:00 2001 From: Tannmay-ts Date: Sun, 18 Oct 2026 22:54:08 +0530 Subject: [PATCH] fix(parameters): wrap GetParameters client errors in get_parameters_by_name _get_parameters_by_name only handled InvalidKeyId and InternalServerError, so throttling or any other ClientError escaped raw in both modes. And in fail-fast mode, those two handled errors returned failed names that the caller discarded, so parameters silently went missing. Any failure of the GetParameters request now raises GetParameterError when raise_on_error=True, and reports the request's names under _errors when raise_on_error=False. Closes #8597 Co-Authored-By: Claude Opus 5.5 --- .../utilities/parameters/ssm.py | 9 ++-- .../_boto3/test_utilities_parameters.py | 51 +++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index 6ce4f853459..dffbdb1aed7 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -622,15 +622,16 @@ def _get_parameters_by_name( batch_errors: list[str] = [] parameter_names = list(parameters.keys()) - # All params in the batch must be decrypted - # we return early if we hit an unrecoverable exception like InvalidKeyId/InternalServerError - # everything else should technically be recoverable as GetParameters is non-atomic + # Handle request-level failures according to raise_on_error. + # Successful responses may still report failures in InvalidParameters. try: if decrypt: response = self.client.get_parameters(Names=parameter_names, WithDecryption=True) else: response = self.client.get_parameters(Names=parameter_names) - except (self.client.exceptions.InvalidKeyId, self.client.exceptions.InternalServerError): + except Exception as exc: + if raise_on_error: + raise GetParameterError(str(exc)) from exc return ret, parameter_names batch_errors = self._handle_any_invalid_get_parameter_errors(response, raise_on_error) diff --git a/tests/functional/parameters/_boto3/test_utilities_parameters.py b/tests/functional/parameters/_boto3/test_utilities_parameters.py index dc7643795c1..908758a91a6 100644 --- a/tests/functional/parameters/_boto3/test_utilities_parameters.py +++ b/tests/functional/parameters/_boto3/test_utilities_parameters.py @@ -1193,6 +1193,57 @@ def test_ssm_provider_get_parameters_by_name_do_not_raise_on_failure_with_decryp stubber.deactivate() +@pytest.mark.parametrize("decrypt", [False, True]) +@pytest.mark.parametrize("error_code", ["ThrottlingException", "InvalidKeyId", "InternalServerError"]) +def test_ssm_provider_get_parameters_by_name_wraps_client_error(mock_name, config, error_code, decrypt): + # GIVEN the GetParameters call fails with an SDK error + param = f"/{mock_name}" + expected_params = {"Names": [param], "WithDecryption": True} if decrypt else {"Names": [param]} + + provider = parameters.SSMProvider(boto_config=config) + stubber = stub.Stubber(provider.client) + stubber.add_client_error("get_parameters", error_code, expected_params=expected_params) + stubber.activate() + + # WHEN fail-fast is enabled in get_parameters_by_name + # THEN the SDK error should be wrapped in GetParameterError + try: + with pytest.raises(parameters.exceptions.GetParameterError, match=error_code): + provider.get_parameters_by_name(parameters={param: {}}, decrypt=decrypt) + stubber.assert_no_pending_responses() + finally: + stubber.deactivate() + + +def test_ssm_provider_get_parameters_by_name_do_not_raise_on_client_error(mock_name, mock_value, config): + # GIVEN 11 parameters, fetched in two GetParameters chunks + # the first chunk has one invalid parameter and the second chunk is throttled + names = [f"/{mock_name}/{i}" for i in range(11)] + first_chunk = dict.fromkeys(names[:10], mock_value) + invalid, throttled = names[9], names[10] + + provider = parameters.SSMProvider(boto_config=config) + stubber = stub.Stubber(provider.client) + stubber.add_response( + "get_parameters", + build_get_parameters_stub(params=first_chunk, invalid_parameters=[invalid]), + {"Names": names[:10]}, + ) + stubber.add_client_error("get_parameters", "ThrottlingException", expected_params={"Names": [throttled]}) + stubber.activate() + + # WHEN fail-fast is disabled in get_parameters_by_name + try: + ret = provider.get_parameters_by_name(parameters={name: {} for name in names}, raise_on_error=False) + stubber.assert_no_pending_responses() + + # THEN both failures should be reported under `_errors` key and the successful values kept + assert ret["_errors"] == [invalid, throttled] + assert all(ret[name] == mock_value for name in names[:9]) + finally: + stubber.deactivate() + + def test_ssm_provider_get_parameters_by_name_do_not_raise_on_failure_batch_decrypt_combined( mock_value, mock_version,