halo-dev/halo · error · DuplicateKeyException

Duplicate key '{}' for index '{}'

Error message

Duplicate key '{}' for index '{}'

What it means

Thrown as org.springframework.dao.DuplicateKeyException in the prepare() preflight of SingleValueIndex.UpsertTransactionalOperation. Before any mutation, it checks whether the index is unique, the new key differs from the row's previous key, and the new key already maps to other primary keys. If so it aborts the transaction before commit, so no partial state is written.

Source

Thrown at application/src/main/java/run/halo/app/extension/index/SingleValueIndex.java:281

        private boolean committed;

        UpsertTransactionalOperation(String primaryKey, @Nullable K newKey) {
            this.primaryKey = primaryKey;
            this.newKey = newKey;
        }

        @Override
        public void prepare() {
            // preflight checks
            if (!spec.isNullable() && newKey == null) {
                throw new IllegalArgumentException("Index %s of %s is not nullable".formatted(getName(), primaryKey));
            }
            previousKey = invertedIndex.get(primaryKey);
            previousNull = nullKeyValues.contains(primaryKey);
            if (isUnique() && newKey != null && !Objects.equals(previousKey, newKey)) {
                var existingPrimaryKeys = index.get(newKey);
                if (!CollectionUtils.isEmpty(existingPrimaryKeys)) {
                    throw new DuplicateKeyException("Duplicate key '" + newKey + "' for index '" + getName() + "'");
                }
            }
        }

        @Override
        public void commit() {
            if (committed) {
                return;
            }
            committed = true;
            removeKey(primaryKey, previousKey);
            addKey(primaryKey, newKey);
        }

        @Override
        public void rollback() {
            if (!committed) {
                return;

View on GitHub (pinned to d2f5165f9c)

Solutions

  1. Resolve the collision: choose another value, or delete/update the owner of the existing key first.
  2. Guard the update with a pre-check via the indexer/query API for the candidate key before attempting the write.
  3. Verify the extension's index schema really should be unique; if not, relax it at the schema level.
  4. Retry with exponential backoff only if the collision is from a concurrent transient state you intend to clear.

Example fix

// before
extensionClient.update(resource).block(); // newKey collides

// after
boolean taken = queryIndex.equal("fieldName", candidateValue).stream().anyMatch(id -> !id.equals(resource.getMetadata().getName()));
if (taken) {
    throw new IllegalStateException("Value already in use: " + candidateValue);
}
extensionClient.update(resource).block();
Defensive patterns

Strategy: validation

Validate before calling

// Preflight: is the new single-value key free for someone other than self?
Set<String> existing = valueIndexQuery.equal("<field>", newKey);
boolean collision = !existing.isEmpty() && !existing.equals(Set.of(selfPrimaryKey));
if (collision) throw new IllegalStateException("Key taken: " + newKey);

Try / catch

try {
    indexer.update(...); // goes through prepare()->commit()
} catch (DuplicateKeyException e) {
    throw new ConflictException("Rename target already in use", e);
}

Prevention

When it happens

Trigger: Updating a single-value unique index to a new value that another resource already owns: isUnique()==true, newKey != null, !Objects.equals(previousKey, newKey), and index.get(newKey) returns a non-empty primary-key set.

Common situations: Renaming a uniquely-indexed field (e.g. username, slug) to a value already in use; bulk re-import that assigns colliding keys; concurrent edits where two requests target the same free value.

Related errors


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