apache/pulsar · error · RestException

ClusterIds should not be null or empty

Error message

ClusterIds should not be null or empty

What it means

HTTP 412 (PRECONDITION_FAILED) returned by the set-replication-clusters admin API when the clusterIds list is null or empty. A namespace must always replicate to at least one cluster, so clearing the list entirely is rejected before any policy write occurs.

Solutions

  1. Pass at least one valid cluster id in clusterIds.
  2. Fix the template/config that resolved to an empty list — set a default local cluster.
  3. To change replication, supply the new full list (e.g. [local]) rather than emptying it.
  4. Add client-side preflight: reject empty clusterIds before calling the API.

Example fix

// before
setReplicationClusters(ns, clusters); // clusters = []
// after
if (clusters == null || clusters.isEmpty()) {
    clusters = List.of(localClusterId); // never empty
}
setReplicationClusters(ns, clusters);
Defensive patterns

Strategy: validation

Validate before calling

if (clusterIds == null || clusterIds.isEmpty()) {
    throw new IllegalArgumentException("clusterIds must contain at least one cluster");
}

Try / catch

try {
    admin.namespaces().setNamespaceReplicationClusters(ns, clusterIds);
} catch (PulsarAdminException.PreconditionFailedException e) {
    log.error("Refusing empty replication cluster set for {}", ns);
}

Prevention

When it happens

Trigger: POST /namespaces/{tenant}/{namespace}/replication with a null body field or an empty JSON array for clusterIds, via internalSetNamespaceReplicationClusters.

Common situations: Automation templating that renders an empty cluster list (unset config value); code that computes clusters dynamically and produces []; attempting to 'disable' replication by sending an empty list.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/c001b8dedd63e918. Report an issue: GitHub.

Appendix: source

Thrown at pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/NamespacesBase.java:857

    }

    /**
     * Directly get the replication clusters for a namespace, without checking allowed clusters.
     */
    protected CompletableFuture<Set<String>> internalGetNamespaceReplicationClustersAsync() {
        return validateNamespacePolicyOperationAsync(namespaceName, PolicyName.REPLICATION, PolicyOperation.READ)
                .thenCompose(__ -> getNamespacePoliciesAsync(namespaceName))
                .thenApply(policies -> policies.replication_clusters);
    }

    @SuppressWarnings("checkstyle:WhitespaceAfter")
    protected CompletableFuture<Void> internalSetNamespaceReplicationClusters(List<String> clusterIds,
                                                                              boolean compareTopicPartitions) {
        return validateNamespacePolicyOperationAsync(namespaceName, PolicyName.REPLICATION, PolicyOperation.WRITE)
                .thenCompose(__ -> validatePoliciesReadOnlyAccessAsync())
                .thenApply(__ -> {
                    if (CollectionUtils.isEmpty(clusterIds)) {
                        throw new RestException(Status.PRECONDITION_FAILED, "ClusterIds should not be null or empty");
                    }
                    return Sets.newHashSet(clusterIds);
                }).thenCompose(replicationClusterSet -> clustersAsync()
                        .thenCompose(clusters -> {
                            List<CompletableFuture<Void>> futures =
                                    replicationClusterSet.stream().map(clusterId -> {
                                        if (!clusters.contains(clusterId)) {
                                            throw new RestException(Status.FORBIDDEN,
                                                    "Invalid cluster id: " + clusterId);
                                        }
                                        return validatePeerClusterConflictAsync(clusterId, replicationClusterSet)
                                            .thenCompose(__ -> getNamespacePoliciesAsync(this.namespaceName)
                                                .thenCompose(nsPolicies -> {
                                                    if (!Policies.checkNewReplicationClusters(nsPolicies,
                                                            replicationClusterSet)) {
                                                        String msg = String.format("Cluster [%s] is not in the "
                                                                + "list of allowed clusters list for namespace "
                                                                + "[%s]", clusterId, namespaceName.toString());

View on GitHub (pinned to 820761864e)