diff --git a/aws_lambda_powertools/utilities/parameters/ssm.py b/aws_lambda_powertools/utilities/parameters/ssm.py index a7c0d2cbeb5..b289df89246 100644 --- a/aws_lambda_powertools/utilities/parameters/ssm.py +++ b/aws_lambda_powertools/utilities/parameters/ssm.py @@ -622,17 +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) as exc: + except Exception as exc: if raise_on_error: - raise GetParameterError(f"Failed to fetch parameters: {parameter_names}") from exc + raise GetParameterError(f"Failed to fetch parameters: {parameter_names}: {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 ace75d6d373..1f20deba25e 100644 --- a/tests/functional/parameters/_boto3/test_utilities_parameters.py +++ b/tests/functional/parameters/_boto3/test_utilities_parameters.py @@ -1238,6 +1238,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,