apache/pulsar · error · IllegalArgumentException

${topic} is invalid. Expected format: '<domain>://tenant/nam

Error message

${topic} is invalid. Expected format: '<domain>://tenant/namespace/topic'

What it means

toFullTopicName expects a full topic URL of the form '<domain>://<tenant>/<namespace>/<local-topic>' — exactly 3 segments after '://'. If the string contains '://' but splits into any count other than 3 or 4 (4 being the rejected V1 form), the method throws this IllegalArgumentException stating the expected format.

Source

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

     * For convenience, clients can pass a short topic name:
     * - "<local-topic>", which represents "persistent://public/default/<local-topic>"
     * - "<tenant>/<namespace>/<local-topic>", which represents "persistent://<tenant>/<namespace>/<local-topic>"
     *
     * @param topic the topic name from client
     * @return the full topic name.
     */
    public static String toFullTopicName(String topic) {
        final int index = topic.indexOf("://");
        if (index >= 0) {
            TopicDomain.getEnum(topic.substring(0, index));
            final List<String> parts = splitBySlash(topic.substring(index + "://".length()), 4);
            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: " + topic);
            }
            if (parts.size() != 3) {
                throw new IllegalArgumentException(topic + " is invalid. "
                    + "Expected format: '<domain>://tenant/namespace/topic'");
            }
            NamespaceName.validateNamespaceName(parts.get(0), parts.get(1));
            if (StringUtils.isBlank(parts.get(2))) {
                throw new IllegalArgumentException(topic + " has blank local topic");
            }
            return topic; // it's a valid full topic name
        } else {
            List<String> parts = splitBySlash(topic, 0);
            if (parts.size() != 1 && parts.size() != 3) {
                throw new IllegalArgumentException(topic + " is invalid");
            }
            if (parts.size() == 1) {
                if (StringUtils.isBlank(parts.get(0))) {
                    throw new IllegalArgumentException(topic + " has blank local topic");
                }
                return "persistent://public/default/" + parts.get(0);
            } else {

View on GitHub (pinned to 820761864e)

Solutions

  1. Use exactly '<domain>://<tenant>/<namespace>/<local-topic>' with 3 slash-separated parts after '://'
  2. Count the segments in your input; fix missing or extra segments
  3. For short names, omit '://' entirely and let Pulsar apply the default (see short-name handling)

Example fix

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

Strategy: validation

Validate before calling

public static boolean isWellFormedTopicUrl(String s) {
    if (s == null) return false;
    int i = s.indexOf("://");
    if (i < 0) return false;
    return s.substring(i + 3).split("/").length == 3;
}

Try / catch

try {
    String full = TopicName.toFullTopicName(raw);
} catch (IllegalArgumentException e) {
    log.error("Topic '{}' does not match '<domain>://tenant/namespace/topic': {}", raw, e.getMessage());
}

Prevention

When it happens

Trigger: Calling toFullTopicName() with a name like 'persistent://tenant' or 'persistent://tenant/ns' (too few segments) or one with extra segments (5+), e.g. 'persistent://tenant/ns/topic/extra'.

Common situations: Typos in config (missing namespace or topic segment); string concatenation bugs producing extra slashes/segments; confusing a REST path with a topic URL; partitioned-topic suffix handling gone wrong.

Related errors


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