{"record":{"id":"182fea0a8956d2b4","repo":"floci-io/floci","slug":"idempotencyexception","errorCode":"IdempotencyException","errorMessage":"An idempotency token was used with a request that does not match a previous request that used that token.","messagePattern":"An idempotency token was used with a request that does not match a previous request that used that token\\.","errorType":"http","errorClass":"AwsException","httpStatus":400,"severity":"error","filePath":"src/main/java/io/github/hectorvent/floci/services/acm/AcmService.java","lineNumber":524,"sourceCode":"     * @throws AwsException if token exists but parameters don't match (IdempotencyTokenException)\n     */\n    private Optional<Certificate> findByIdempotencyToken(String token, String region, int requestHash) {\n        String indexKey = region + \"::\" + token;\n        IdempotencyTokenEntry entry = idempotencyTokenIndex.get(indexKey);\n\n        if (entry == null) {\n            return Optional.empty();\n        }\n\n        // Lazy expiration: remove expired entries on lookup\n        if (entry.isExpired()) {\n            idempotencyTokenIndex.remove(indexKey);\n            return Optional.empty();\n        }\n\n        // Validate request parameters match\n        if (entry.requestHash() != requestHash) {\n            throw new AwsException(\"IdempotencyException\",\n                \"An idempotency token was used with a request that does not match a previous request \" +\n                \"that used that token.\", 400);\n        }\n\n        String certId = extractCertificateIdFromArn(entry.arn());\n        return store.get(regionKey(region, certId));\n    }\n\n    /**\n     * Computes a hash of request parameters for idempotency validation.\n     * Parameters include: domainName, SANs (order-independent), keyAlgorithm.\n     */\n    private int computeRequestHash(String domainName, List<String> sans, KeyAlgorithm keyAlgorithm) {\n        return Objects.hash(\n            domainName,\n            sans != null ? new HashSet<>(sans) : null,\n            keyAlgorithm\n        );","sourceCodeStart":506,"sourceCodeEnd":542,"githubUrl":"https://github.com/floci-io/floci/blob/62ff490619e7bd3554597c28c704081b4c15add5/src/main/java/io/github/hectorvent/floci/services/acm/AcmService.java#L506-L542","documentation":"ACM IdempotencyException (HTTP 400) thrown during requestCertificate idempotency lookup: a stored token entry exists and is unexpired, but computeRequestHash of the incoming request (Objects.hash of domainName, SANs as a set, keyAlgorithm) differs from the hash recorded when the token was first used. ACM requires the same token be reused only with identical request parameters.","triggerScenarios":"requestCertificate(..., idempotencyToken=T) with domainName/SANs/keyAlgorithm differing from an earlier call that used the same token T within the token's validity window. Note SAN comparison is order-insensitive (HashSet), but adding/removing a SAN, changing the domain, or switching keyAlgorithm changes the hash.","commonSituations":"Retrying a failed request but with 'fixed' parameters and the same token; token generators derived from request count that collide across differing requests; test suites reusing hardcoded tokens ('token-1') across different certificate requests.","solutions":["Reuse an idempotency token only for retries of the exact same request; generate a fresh token whenever parameters change","Derive the token deterministically from the request content (e.g. UUID5/sha256 of domain+SANs+algorithm) so identical params yield identical tokens and differing params never collide","In tests, use unique tokens per test case or reset the emulator between parameter-changing scenarios"],"exampleFix":"// before: same static token for different requests\nacm.requestCertificate(r -> r.domainName(\"a.example.com\").idempotencyToken(\"tok\"));\nacm.requestCertificate(r -> r.domainName(\"b.example.com\").idempotencyToken(\"tok\")); // IdempotencyException\n\n// after: token derived from request content\nString token = UUID.nameUUIDFromBytes((domain + \"|\" + String.join(\",\", sans) + \"|\" + alg).getBytes()).toString();\nacm.requestCertificate(r -> r.domainName(domain).idempotencyToken(token));","handlingStrategy":"validation","validationCode":"String token = UUID.nameUUIDFromBytes(\n    (domain + \"|\" + (sans == null ? \"\" : String.join(\",\", sans.stream().sorted().toList())) + \"|\" + alg)\n    .getBytes(StandardCharsets.UTF_8)).toString();\n// identical params -> identical token; different params -> different token, no collision","typeGuard":null,"tryCatchPattern":"try {\n    acm.requestCertificate(r -> r.domainName(d).idempotencyToken(tok));\n} catch (IdempotencyException e) {\n    // same token used with different params — mint a fresh token and retry\n}","preventionTips":["Never reuse an idempotency token after changing any request parameter","Derive tokens from request content so identity is deterministic","Use unique tokens per test case; never hardcode 'token-1' across scenarios"],"tags":["acm","idempotency","request-certificate","validation","aws"],"backgroundTag":null,"analyzedSha":"62ff490619e7bd3554597c28c704081b4c15add5","analyzedAt":"2026-08-14T14:25:23.764Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}