halo-dev/halo · error · DuplicateKeyException

Duplicate key '%s' for extension '%s'

Error message

Duplicate key '%s' for extension '%s'

What it means

Thrown as org.springframework.dao.DuplicateKeyException during the commit phase of a multi-value index upsert (MultiValueIndex.UpsertTransactionalOperation.commit). It fires only when the index spec is marked unique and a key in newKeys already maps to one or more different primary keys, so accepting it would break the uniqueness invariant. Halo's extension store uses this to enforce unique constraints on extension fields (e.g. a unique name or slug).

Source

Thrown at application/src/main/java/run/halo/app/extension/index/MultiValueIndex.java:245

                return;
            }
            invertedIndex.put(primaryKey, newKeys);
            // remove previous keys
            if (!CollectionUtils.isEmpty(previousKeys)) {
                previousKeys.forEach(key -> index.computeIfPresent(key, (k, v) -> {
                    v.remove(primaryKey);
                    return v.isEmpty() ? null : v;
                }));
            }
            // add new keys
            if (!CollectionUtils.isEmpty(newKeys)) {
                for (K key : newKeys) {
                    index.compute(key, (k, v) -> {
                        if (v == null) {
                            v = ConcurrentHashMap.newKeySet();
                        }
                        if (spec.isUnique() && !v.isEmpty()) {
                            throw new DuplicateKeyException(
                                    String.format("Duplicate key '%s' for extension '%s'", k, primaryKey));
                        }
                        v.add(primaryKey);
                        return v;
                    });
                }
                nullKeyValues.remove(primaryKey);
            } else {
                nullKeyValues.add(primaryKey);
            }
        }

        @Override
        public void rollback() {
            if (Objects.equals(this.previousKeys, newKeys) || !committed) {
                return;
            }
            // remove possibly added new keys

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. Look up the existing extension by the colliding key value before create/update and reuse or skip it instead of inserting a duplicate.
  2. Pick a different, unique value for the indexed field and retry the create/update.
  3. If duplicates are legitimately expected, change the index spec so isUnique() is false (schema/extension definition change, not a runtime fix).
  4. Delete the conflicting older resource first, then re-attempt the write.

Example fix

// before
client.create(dupeNamedResource).block();

// after
var existing = indexer.all().stream()
    .filter(id -> "the-colliding-value".equals(readField(id))).findAny();
if (existing.isPresent()) {
    return; // or update existing instead
}
client.create(dupeNamedResource).block();
Defensive patterns

Strategy: validation

Validate before calling

// Before create/update, query the unique index for the candidate key
Set<String> owners = valueIndexQuery.equal("<indexedField>", candidateKey);
boolean willCollide = owners.stream().anyMatch(id -> !id.equals(currentPrimaryKey));
if (willCollide) {
    return Mono.error(new IllegalStateException("Value already used: " + candidateKey));
}

Type guard

// Java has no structural type guard; validate via index key type
static <K> boolean isUniqueSafe(ValueIndexQuery<K> idx, K candidate, String selfId) {
    return idx.equal(candidate).stream().noneMatch(id -> !id.equals(selfId));
}

Try / catch

try {
    extensionClient.create(resource).block();
} catch (DuplicateKeyException e) {
    // surface a user-friendly 'duplicate value' message; offer the existing resource
    throw new ConflictException("A resource with this value already exists", e);
}

Prevention

When it happens

Trigger: Creating or updating an extension whose indexed field is declared unique, where the supplied value collides with a value already stored under a different extension name/primary key. Concretely: a second extension resource is created/updated so that its computed index key set overlaps an existing key while spec.isUnique()==true and the existing entry set for that key is non-empty.

Common situations: Duplicate display names or slugs on resources that carry a unique index; two users/plugins racing to register the same identifier; a migration or import that re-inserts rows whose unique field already exists; renaming a resource to a name another resource already holds.

Related errors


AI-assisted analysis of halo-dev/halo@d2f5165f9c (2026-08-14). Data as JSON: /api/errors/dc42501ccd9b3a51. Report an issue: GitHub.