floci-io/floci · error · AwsException
InvalidIfMatchVersion
InvalidIfMatchVersion
Error message
The If-Match version is missing or not valid for the resource.
What it means
Floci's CloudFront service layer compares the caller's If-Match value against the distribution's stored ETag on UpdateDistribution. Unlike the controller-side checks (missing header), this fires when a header WAS sent but does not equal the current ETag — the emulator rotates the ETag (UUID) on every update, so any stale value fails.
Source
Thrown at src/main/java/io/github/hectorvent/floci/services/cloudfront/CloudFrontService.java:142
dist.setLastModifiedTime(Instant.now());
dist.setEtag(UUID.randomUUID().toString());
if (tags != null && !tags.isEmpty()) {
dist.setTags(tags);
tagStore.put("distribution/" + id, tags);
}
distStore.put(id, dist);
return dist;
}
public Distribution getDistribution(String id) {
return distStore.get(id).orElseThrow(() ->
new AwsException("NoSuchDistribution", "The specified distribution does not exist.", 404));
}
public synchronized Distribution updateDistribution(String id, String ifMatch, Distribution updated) {
Distribution existing = getDistribution(id);
if (!existing.getEtag().equals(ifMatch)) {
throw new AwsException("InvalidIfMatchVersion",
"The If-Match version is missing or not valid for the resource.", 400);
}
ensureAliasesAvailable(updated.getConfig(), id);
validateTrustedKeyGroups(updated.getConfig());
updated.setId(id);
updated.setArn(existing.getArn());
updated.setDomainName(existing.getDomainName());
updated.setStatus("Deployed");
updated.setLastModifiedTime(Instant.now());
updated.setEtag(UUID.randomUUID().toString());
updated.setTags(existing.getTags());
distStore.put(id, updated);
return updated;
}
public synchronized void deleteDistribution(String id, String ifMatch) {
Distribution existing = getDistribution(id);
if (!existing.getEtag().equals(ifMatch)) {View on GitHub (pinned to 62ff490619)
Solutions
- Re-GET the distribution config immediately before each PUT and use the fresh ETag
- On this error, treat it as a conflict: reload the config, reapply your changes, and retry once
- Never reuse an ETag after a successful update — it is regenerated per write
Example fix
// before: etag cached once and reused cf.updateDistribution(req -> req.id(id).ifMatch(cachedEtag).distributionConfig(cfg)); // after: refresh each time String etag = cf.getDistributionConfig(r -> r.id(id)).eTag(); cf.updateDistribution(req -> req.id(id).ifMatch(etag).distributionConfig(cfg));
Defensive patterns
Strategy: retry
Validate before calling
// precondition: ETag must be the one from the most recent read
String current = cf.getDistributionConfig(r -> r.id(id)).eTag();
if (!current.equals(ifMatchValue)) {
// stale: reload config and merge your changes before updating
} Try / catch
for (int attempt = 0; attempt < 2; attempt++) {
try {
String etag = cf.getDistributionConfig(r -> r.id(id)).eTag();
cf.updateDistribution(req -> req.id(id).ifMatch(etag).distributionConfig(cfg));
break;
} catch (CloudFrontException e) {
if ("InvalidIfMatchVersion".equals(e.awsErrorDetails().errorCode()) && attempt == 0) {
continue; // ETag raced; re-read and retry once
}
throw e;
}
} Prevention
- Scope ETag lifetime to a single read-modify-write cycle
- Expect the emulator (like AWS) to mint a new ETag per update; never cache across writes
When it happens
Trigger: PUT /distribution/{Id}/config with an If-Match value that differs from the stored ETag: reused ETag from an earlier update, ETag of a different distribution, or a value like '*' that the emulator does not special-case.
Common situations: Caching the ETag across multiple updates instead of re-reading after each one; concurrent writers where one update invalidates the other's ETag; assuming '*' is accepted as any-version.
Related errors
AI-assisted analysis of floci-io/floci@62ff490619 (2026-08-14).
Data as JSON: /api/errors/704799e3f3bb65b5.
Report an issue: GitHub.