diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md
index 240752a55..a6da0e37e 100644
--- a/docs/utilities/idempotency.md
+++ b/docs/utilities/idempotency.md
@@ -55,7 +55,7 @@ times with the same parameters**. This makes idempotent operations safe to retry
software.amazon.lambda
- powertools-idempotency-dynamodb
+ powertools-idempotency-core
@@ -584,6 +584,7 @@ IdempotencyConfig.builder()
.withUseLocalCache(true)
.withLocalCacheMaxItems(432)
.withHashFunction("SHA-256")
+ .withResponseHook((responseData, dataRecord) -> responseData)
.build()
```
@@ -591,13 +592,14 @@ These are the available options for further configuration:
| Parameter | Default | Description |
|---------------------------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------|
-| **EventKeyJMESPath** | `""` | JMESPath expression to extract the idempotency key from the event record. See available [built-in functions](serialization) |
+| **EventKeyJMESPath** | `""` | JMESPath expression to extract the idempotency key from the event record. See available [built-in functions](serialization) |
| **PayloadValidationJMESPath** | `""` | JMESPath expression to validate whether certain parameters have changed in the event |
-| **ThrowOnNoIdempotencyKey** | `False` | Throw exception if no idempotency key was found in the request |
+| **ThrowOnNoIdempotencyKey** | `false` | Throw exception if no idempotency key was found in the request |
| **ExpirationInSeconds** | 3600 | The number of seconds to wait before a record is expired |
| **UseLocalCache** | `false` | Whether to locally cache idempotency results (LRU cache) |
| **LocalCacheMaxItems** | 256 | Max number of items to store in local cache |
| **HashFunction** | `MD5` | Algorithm to use for calculating hashes, as supported by `java.security.MessageDigest` (eg. SHA-1, SHA-256, ...) |
+| **ResponseHook** | `null` | Response hook to apply modifications to idempotent responses |
These features are detailed below.
@@ -855,6 +857,58 @@ You can extend the `BasePersistenceStore` class and implement the abstract metho
For example, the `putRecord` method needs to throw an exception if a non-expired record already exists in the data store with a matching key.
+### Manipulating the Idempotent Response
+
+You can set up a response hook in the Idempotency configuration to manipulate the returned data when an operation is idempotent. The hook function will be called with the current de-serialized response `Object` and the Idempotency `DataRecord`.
+
+The example below shows how to append an HTTP header to an `APIGatewayProxyResponseEvent`.
+
+=== "Using an Idempotent Response Hook"
+
+ ```java hl_lines="3-20"
+ Idempotency.config().withConfig(
+ IdempotencyConfig.builder()
+ .withResponseHook((responseData, dataRecord) -> {
+ if (responseData instanceof APIGatewayProxyResponseEvent) {
+ APIGatewayProxyResponseEvent proxyResponse =
+ (APIGatewayProxyResponseEvent) responseData;
+ final Map headers = new HashMap<>();
+ headers.putAll(proxyResponse.getHeaders());
+ // Append idempotency headers
+ headers.put("x-idempotency-response", "true");
+ headers.put("x-idempotency-expiration",
+ String.valueOf(dataRecord.getExpiryTimestamp()));
+
+ proxyResponse.setHeaders(headers);
+
+ return proxyResponse;
+ }
+
+ return responseData;
+ })
+ .build())
+ .withPersistenceStore(
+ DynamoDBPersistenceStore.builder()
+ .withTableName(System.getenv("IDEMPOTENCY_TABLE"))
+ .build())
+ .configure();
+ ```
+
+???+ info "Info: Using custom de-serialization?"
+
+ The response hook is called after de-serialization so the payload you process will be the de-serialized Java object.
+
+#### Being a good citizen
+
+When using response hooks to manipulate returned data from idempotent operations, it's important to follow best practices to avoid introducing complexity or issues. Keep these guidelines in mind:
+
+1. **Response hook works exclusively when operations are idempotent.** The hook will not be called when an operation is not idempotent, or when the idempotent logic fails.
+
+2. **Catch and Handle Exceptions.** Your response hook code should catch and handle any exceptions that may arise from your logic. Unhandled exceptions will cause the Lambda function to fail unexpectedly.
+
+3. **Keep Hook Logic Simple** Response hooks should consist of minimal and straightforward logic for manipulating response data. Avoid complex conditional branching and aim for hooks that are easy to reason about.
+
+
## Compatibility with other utilities
### Validation utility
diff --git a/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java
index 0c4693230..029877c73 100644
--- a/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java
+++ b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java
@@ -45,20 +45,36 @@ public App() {
public App(DynamoDbClient client) {
Idempotency.config().withConfig(
- IdempotencyConfig.builder()
- .withEventKeyJMESPath(
- "powertools_json(body).address") // will retrieve the address field in the body which is a string transformed to json with `powertools_json`
- .build())
+ IdempotencyConfig.builder()
+ .withEventKeyJMESPath("powertools_json(body).address")
+ .withResponseHook((responseData, dataRecord) -> {
+ if (responseData instanceof APIGatewayProxyResponseEvent) {
+ APIGatewayProxyResponseEvent proxyResponse = (APIGatewayProxyResponseEvent) responseData;
+ final Map headers = new HashMap<>();
+ headers.putAll(proxyResponse.getHeaders());
+ headers.put("x-idempotency-response", "true");
+ headers.put("x-idempotency-expiration",
+ String.valueOf(dataRecord.getExpiryTimestamp()));
+
+ proxyResponse.setHeaders(headers);
+
+ return proxyResponse;
+ }
+
+ return responseData;
+ })
+ .build())
.withPersistenceStore(
DynamoDBPersistenceStore.builder()
.withDynamoDbClient(client)
.withTableName(System.getenv("IDEMPOTENCY_TABLE"))
- .build()
- ).configure();
+ .build())
+ .configure();
}
/**
- * This is our Lambda event handler. It accepts HTTP POST requests from API gateway and returns the contents of the given URL. Requests are made idempotent
+ * This is your Lambda event handler. It accepts HTTP POST requests from API gateway and returns the contents of the
+ * given URL. Requests are made idempotent
* by the idempotency library, and results are cached for the default 1h expiry time.
*
* You can test the endpoint like this:
@@ -67,8 +83,10 @@ public App(DynamoDbClient client) {
* curl -X POST https://[REST-API-ID].execute-api.[REGION].amazonaws.com/Prod/helloidem/ -H "Content-Type: application/json" -d '{"address": "https://checkip.amazonaws.com"}'
*
*
- *
First call will execute the handleRequest normally, and store the response in the idempotency table (Look into DynamoDB)
- *
Second call (and next ones) will retrieve from the cache (if cache is enabled, which is by default) or from the store, the handler won't be called. Until the expiration happens (by default 1 hour).
+ *
First call will execute the handleRequest normally, and store the response in the idempotency table (Look
+ * into DynamoDB)
+ *
Second call (and next ones) will retrieve from the cache (if cache is enabled, which is by default) or from
+ * the store, the handler won't be called. Until the expiration happens (by default 1 hour).
*
*/
@Idempotent // The magic is here!
@@ -101,14 +119,14 @@ public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEv
}
}
-
/**
* Helper to retrieve the contents of the given URL and return them as a string.
*
* We could also put the @Idempotent annotation here if we only wanted this sub-operation to be idempotent. Putting
* it on the handler, however, reduces total execution time and saves us time!
*
- * @param address The URL to fetch
+ * @param address
+ * The URL to fetch
* @return The contents of the given URL
* @throws IOException
*/
@@ -118,4 +136,4 @@ private String getPageContents(String address) throws IOException {
return br.lines().collect(Collectors.joining(System.lineSeparator()));
}
}
-}
\ No newline at end of file
+}
diff --git a/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java
index 2b22cac51..9d5c66cac 100644
--- a/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java
+++ b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java
@@ -14,10 +14,13 @@
package software.amazon.lambda.powertools.idempotency;
+import java.time.Duration;
+import java.util.function.BiFunction;
+
import com.amazonaws.services.lambda.runtime.Context;
-import software.amazon.lambda.powertools.idempotency.internal.cache.LRUCache;
-import java.time.Duration;
+import software.amazon.lambda.powertools.idempotency.internal.cache.LRUCache;
+import software.amazon.lambda.powertools.idempotency.persistence.DataRecord;
/**
* Configuration of the idempotency feature. Use the {@link Builder} to create an instance.
@@ -30,11 +33,12 @@ public class IdempotencyConfig {
private final String payloadValidationJMESPath;
private final boolean throwOnNoIdempotencyKey;
private final String hashFunction;
+ private final BiFunction