{"record":{"id":"2a71753638babc98","repo":"apache/pulsar","slug":"invalid-topic-name-completetopicname","errorCode":null,"errorMessage":"Invalid topic name ${completeTopicName}","messagePattern":"Invalid topic name (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"pulsar-common/src/main/java/org/apache/pulsar/common/naming/TopicName.java","lineNumber":192,"sourceCode":"                this.segmentRange = null;\n                this.segmentId = -1;\n                this.completeTopicName = domain.name() + \"://\" + tenant + \"/\" + namespacePortion + \"/\" + localName;\n            } else {\n                this.domain = TopicDomain.getEnum(completeTopicName.substring(0, index));\n                // Scalable topic domains (topic://, segment://) only support the new format\n                // and local names may contain '/', so use limit(3) to keep the rest as localName.\n                boolean isScalableDomain = this.domain == TopicDomain.topic\n                        || this.domain == TopicDomain.segment;\n                int splitLimit = isScalableDomain ? 3 : 4;\n                List<String> parts = splitBySlash(completeTopicName.substring(index + \"://\".length()),\n                        splitLimit);\n                if (parts.size() == 4) {\n                    throw new IllegalArgumentException(\n                            \"V1 topic names (with cluster component) are no longer supported. \"\n                                    + \"Please use the V2 format: '<domain>://tenant/namespace/topic'. Got: \"\n                                    + completeTopicName);\n                } else if (parts.size() != 3) {\n                    throw new IllegalArgumentException(\"Invalid topic name \" + completeTopicName);\n                }\n                this.tenant = parts.get(0);\n                this.namespacePortion = parts.get(1);\n                String rawLocalName = parts.get(2);\n\n                // For segment:// domains, split local name into parent topic name + descriptor\n                if (this.domain == TopicDomain.segment) {\n                    int lastSlash = rawLocalName.lastIndexOf('/');\n                    if (lastSlash <= 0) {\n                        throw new IllegalArgumentException(\n                                \"Invalid segment topic name: local name must contain\"\n                                        + \" '<parent-topic>/<hashStart>-<hashEnd>-<segmentId>'. Got: \"\n                                        + completeTopicName);\n                    }\n                    this.localName = rawLocalName.substring(0, lastSlash);\n                    String descriptor = rawLocalName.substring(lastSlash + 1);\n                    String[] descParts = descriptor.split(\"-\");\n                    if (descParts.length != 3) {","sourceCodeStart":174,"sourceCodeEnd":210,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-common/src/main/java/org/apache/pulsar/common/naming/TopicName.java#L174-L210","documentation":"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.","triggerScenarios":"Calling TopicName.get('persistent://tenant') or TopicName.get('persistent://tenant/ns/') — scheme present but path does not split into exactly tenant/namespace/topic.","commonSituations":"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.","solutions":["Supply the complete 'domain://tenant/namespace/topic' string including the local topic name","Check the source of the string (config, env var, API response) for truncation or wrong variable","If you have a namespace not a topic, use NamespaceName/NamespaceName.get instead of TopicName"],"exampleFix":"// before\nTopicName.get(\"persistent://my-tenant/my-ns\");\n// after\nTopicName.get(\"persistent://my-tenant/my-ns/my-topic\");","handlingStrategy":"validation","validationCode":"static boolean isQualifiedTopicNameValid(String name) {\n    if (name == null) return false;\n    int i = name.indexOf(\"://\");\n    if (i < 0) return false;\n    String[] parts = name.substring(i + 3).split(\"/\", -1);\n    return parts.length == 3 && java.util.Arrays.stream(parts).allMatch(p -> !p.isEmpty());\n}","typeGuard":"static boolean isCompleteTopicName(String name) {\n    return name != null && isQualifiedTopicNameValid(name);\n}","tryCatchPattern":"try {\n    TopicName tn = TopicName.get(topic);\n} catch (IllegalArgumentException e) {\n    log.error(\"Malformed topic name '{}'\", topic, e);\n    throw new IllegalArgumentException(\"Provide domain://tenant/namespace/topic, got: \" + topic);\n}","preventionTips":["Log the raw input string when topic parsing fails to spot truncation sources","Use TopicName/TopicName.get early at config-load time so bad values fail fast","Distinguish namespace names from topic names in your data model; use NamespaceName for namespaces","Avoid storing partially-built names; always store the complete 'domain://tenant/ns/topic'"],"tags":["java","illegal-argument","topic-name","pulsar"],"backgroundTag":"invalid-topic-name","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}