From 9d014fc5208f6489e6b08fb8f8ad9169281f7d52 Mon Sep 17 00:00:00 2001 From: Elman Badalov <40571118+thebadalov@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:17:42 +0400 Subject: [PATCH] fix(idempotency): keep the payload hash when completing records in the Redis layer CachePersistenceLayer._update_record overwrote the record with a new SET that only had data, status and expiration, dropping the validation hash written by _put_in_progress_record. With payload_validation_jmespath set, a retry with the same payload then raised IdempotencyValidationError instead of returning the stored response. Write the validation key in _update_record when payload validation is enabled, as the DynamoDB layer already does. --- .../idempotency/persistence/redis.py | 4 +++ .../idempotency/_redis/test_redis_layer.py | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/aws_lambda_powertools/utilities/idempotency/persistence/redis.py b/aws_lambda_powertools/utilities/idempotency/persistence/redis.py index 7b4688beca7..bc3d626f3c6 100644 --- a/aws_lambda_powertools/utilities/idempotency/persistence/redis.py +++ b/aws_lambda_powertools/utilities/idempotency/persistence/redis.py @@ -492,6 +492,10 @@ def _update_record(self, data_record: DataRecord) -> None: self.expiry_attr: data_record.expiry_timestamp, }, } + + if self.payload_validation_enabled: + item["mapping"][self.validation_key_attr] = data_record.payload_hash + logger.debug(f"Updating record for idempotency key: {data_record.idempotency_key}") encoded_item = self._json_serializer(item["mapping"]) ttl = self._get_expiry_second(data_record.expiry_timestamp) diff --git a/tests/functional/idempotency/_redis/test_redis_layer.py b/tests/functional/idempotency/_redis/test_redis_layer.py index e83d2603b61..254ca94f580 100644 --- a/tests/functional/idempotency/_redis/test_redis_layer.py +++ b/tests/functional/idempotency/_redis/test_redis_layer.py @@ -615,6 +615,39 @@ def lambda_handler(event, context): lambda_handler(mock_event, lambda_context) +def test_idempotent_function_and_lambda_handler_redis_validation_same_payload( + persistence_store_standalone_redis: RedisCachePersistenceLayer, + lambda_context, +): + mock_event = {"user_id": "xyz", "time": "1234"} + persistence_layer = persistence_store_standalone_redis + expected_result = {"message": "Foo"} + config = IdempotencyConfig(event_key_jmespath="user_id", payload_validation_jmespath="time") + calls = 0 + + @idempotent(persistence_store=persistence_layer, config=config) + def lambda_handler(event, context): + nonlocal calls + calls += 1 + return expected_result + + # WHEN calling the handler with payload validation enabled + first_result = lambda_handler(mock_event, lambda_context) + + # THEN the completed record keeps the payload hash + stored_record = json.loads(next(iter(persistence_layer.client.cache.values()))) + assert stored_record["status"] == "COMPLETED" + assert stored_record[persistence_layer.validation_key_attr] + + # WHEN calling the handler again with the same payload + second_result = lambda_handler(mock_event, lambda_context) + + # THEN the stored response is returned without running the handler again + assert first_result == expected_result + assert second_result == expected_result + assert calls == 1 + + def test_idempotent_function_and_lambda_handler_redis_basic_no_decode( persistence_store_standalone_redis_no_decode: RedisCachePersistenceLayer, lambda_context,