apache/pulsar · error · IllegalArgumentException

Invalid topic name ${completeTopicName}

Error message

Invalid topic name ${completeTopicName}

What it means

When a fully-qualified name with a 'domain://' scheme is parsed, the path after the scheme must contain exactly 3 slash-separated parts: tenant, namespace portion, and topic local name. Fewer parts (e.g. 'persistent://tenant/ns') or more than the allowed split yields this generic IllegalArgumentException. Note 4 parts instead raises the dedicated V1 error.

Source

Thrown at pulsar-common/src/main/java/org/apache/pulsar/common/naming/TopicName.java:192

                this.segmentRange = null;
                this.segmentId = -1;
                this.completeTopicName = domain.name() + "://" + tenant + "/" + namespacePortion + "/" + localName;
            } else {
                this.domain = TopicDomain.getEnum(completeTopicName.substring(0, index));
                // Scalable topic domains (topic://, segment://) only support the new format
                // and local names may contain '/', so use limit(3) to keep the rest as localName.
                boolean isScalableDomain = this.domain == TopicDomain.topic
                        || this.domain == TopicDomain.segment;
                int splitLimit = isScalableDomain ? 3 : 4;
                List<String> parts = splitBySlash(completeTopicName.substring(index + "://".length()),
                        splitLimit);
                if (parts.size() == 4) {
                    throw new IllegalArgumentException(
                            "V1 topic names (with cluster component) are no longer supported. "
                                    + "Please use the V2 format: '<domain>://tenant/namespace/topic'. Got: "
                                    + completeTopicName);
                } else if (parts.size() != 3) {
                    throw new IllegalArgumentException("Invalid topic name " + completeTopicName);
                }
                this.tenant = parts.get(0);
                this.namespacePortion = parts.get(1);
                String rawLocalName = parts.get(2);

                // For segment:// domains, split local name into parent topic name + descriptor
                if (this.domain == TopicDomain.segment) {
                    int lastSlash = rawLocalName.lastIndexOf('/');
                    if (lastSlash <= 0) {
                        throw new IllegalArgumentException(
                                "Invalid segment topic name: local name must contain"
                                        + " '<parent-topic>/<hashStart>-<hashEnd>-<segmentId>'. Got: "
                                        + completeTopicName);
                    }
                    this.localName = rawLocalName.substring(0, lastSlash);
                    String descriptor = rawLocalName.substring(lastSlash + 1);
                    String[] descParts = descriptor.split("-");
                    if (descParts.length != 3) {

View on GitHub (pinned to 820761864e)

Solutions

  1. Supply the complete 'domain://tenant/namespace/topic' string including the local topic name
  2. Check the source of the string (config, env var, API response) for truncation or wrong variable
  3. If you have a namespace not a topic, use NamespaceName/NamespaceName.get instead of TopicName

Example fix

// before
TopicName.get("persistent://my-tenant/my-ns");
// after
TopicName.get("persistent://my-tenant/my-ns/my-topic");
Defensive patterns

Strategy: validation

Validate before calling

static boolean isQualifiedTopicNameValid(String name) {
    if (name == null) return false;
    int i = name.indexOf("://");
    if (i < 0) return false;
    String[] parts = name.substring(i + 3).split("/", -1);
    return parts.length == 3 && java.util.Arrays.stream(parts).allMatch(p -> !p.isEmpty());
}

Type guard

static boolean isCompleteTopicName(String name) {
    return name != null && isQualifiedTopicNameValid(name);
}

Try / catch

try {
    TopicName tn = TopicName.get(topic);
} catch (IllegalArgumentException e) {
    log.error("Malformed topic name '{}'", topic, e);
    throw new IllegalArgumentException("Provide domain://tenant/namespace/topic, got: " + topic);
}

Prevention

When it happens

Trigger: Calling TopicName.get('persistent://tenant') or TopicName.get('persistent://tenant/ns/') — scheme present but path does not split into exactly tenant/namespace/topic.

Common situations: Truncated topic names in configs or logs; passing a namespace name ('persistent://tenant/namespace') where a topic is expected; programmatic string building that lost the topic segment; empty topic after a trailing slash.

Related errors


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