floci-io/floci · error · AwsException

IdempotencyException

IdempotencyException

Error message

An idempotency token was used with a request that does not match a previous request that used that token.

What it means

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.

Source

Thrown at src/main/java/io/github/hectorvent/floci/services/acm/AcmService.java:524

     * @throws AwsException if token exists but parameters don't match (IdempotencyTokenException)
     */
    private Optional<Certificate> findByIdempotencyToken(String token, String region, int requestHash) {
        String indexKey = region + "::" + token;
        IdempotencyTokenEntry entry = idempotencyTokenIndex.get(indexKey);

        if (entry == null) {
            return Optional.empty();
        }

        // Lazy expiration: remove expired entries on lookup
        if (entry.isExpired()) {
            idempotencyTokenIndex.remove(indexKey);
            return Optional.empty();
        }

        // Validate request parameters match
        if (entry.requestHash() != requestHash) {
            throw new AwsException("IdempotencyException",
                "An idempotency token was used with a request that does not match a previous request " +
                "that used that token.", 400);
        }

        String certId = extractCertificateIdFromArn(entry.arn());
        return store.get(regionKey(region, certId));
    }

    /**
     * Computes a hash of request parameters for idempotency validation.
     * Parameters include: domainName, SANs (order-independent), keyAlgorithm.
     */
    private int computeRequestHash(String domainName, List<String> sans, KeyAlgorithm keyAlgorithm) {
        return Objects.hash(
            domainName,
            sans != null ? new HashSet<>(sans) : null,
            keyAlgorithm
        );

View on GitHub (pinned to 62ff490619)

Solutions

  1. Reuse an idempotency token only for retries of the exact same request; generate a fresh token whenever parameters change
  2. 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
  3. In tests, use unique tokens per test case or reset the emulator between parameter-changing scenarios

Example fix

// before: same static token for different requests
acm.requestCertificate(r -> r.domainName("a.example.com").idempotencyToken("tok"));
acm.requestCertificate(r -> r.domainName("b.example.com").idempotencyToken("tok")); // IdempotencyException

// after: token derived from request content
String token = UUID.nameUUIDFromBytes((domain + "|" + String.join(",", sans) + "|" + alg).getBytes()).toString();
acm.requestCertificate(r -> r.domainName(domain).idempotencyToken(token));
Defensive patterns

Strategy: validation

Validate before calling

String token = UUID.nameUUIDFromBytes(
    (domain + "|" + (sans == null ? "" : String.join(",", sans.stream().sorted().toList())) + "|" + alg)
    .getBytes(StandardCharsets.UTF_8)).toString();
// identical params -> identical token; different params -> different token, no collision

Try / catch

try {
    acm.requestCertificate(r -> r.domainName(d).idempotencyToken(tok));
} catch (IdempotencyException e) {
    // same token used with different params — mint a fresh token and retry
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of floci-io/floci@62ff490619 (2026-08-14). Data as JSON: /api/errors/182fea0a8956d2b4. Report an issue: GitHub.