{"record":{"id":"9bdb8ab619456e10","repo":"apache/pulsar","slug":"invalid-topic-name-s-topic-local-name-must-not","errorCode":null,"errorMessage":"Invalid topic name: %s. Topic local name must not be blank.","messagePattern":"Invalid topic name: (.+?)\\. Topic local name must not be blank\\.","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"pulsar-common/src/main/java/org/apache/pulsar/common/naming/TopicName.java","lineNumber":235,"sourceCode":"                            Integer.parseInt(descParts[1], 16));\n                    this.segmentId = Long.parseLong(descParts[2]);\n                } else {\n                    this.localName = rawLocalName;\n                    this.segmentRange = null;\n                    this.segmentId = -1;\n                }\n\n                if (this.domain == TopicDomain.segment) {\n                    this.completeTopicName = String.format(\"%s://%s/%s/%s/%s\",\n                            domain, tenant, namespacePortion, localName,\n                            String.format(\"%04x-%04x-%d\", segmentRange.start(), segmentRange.end(), segmentId));\n                } else {\n                    this.completeTopicName = completeTopicName;\n                }\n            }\n\n            if (StringUtils.isBlank(localName)) {\n                throw new IllegalArgumentException(String.format(\"Invalid topic name: %s. Topic local name must not\"\n                        + \" be blank.\", completeTopicName));\n            }\n            this.partitionIndex = getPartitionIndex(localName);\n            this.namespaceName = NamespaceName.get(tenant, namespacePortion);\n        } catch (NullPointerException e) {\n            throw new IllegalArgumentException(\"Invalid topic name: \" + completeTopicName, e);\n        }\n    }\n\n    public boolean isPersistent() {\n        return TopicDomain.persistent == domain || TopicDomain.topic == domain || TopicDomain.segment == domain;\n    }\n\n    public boolean isScalable() {\n        return TopicDomain.topic == domain;\n    }\n\n    public boolean isSegment() {","sourceCodeStart":217,"sourceCodeEnd":253,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-common/src/main/java/org/apache/pulsar/common/naming/TopicName.java#L217-L253","documentation":"TopicName's constructor parses a complete topic name into domain, tenant, namespace, and local name. If the final (local) name segment — the topic name itself, optionally with a -partition-N suffix — is blank or missing, the constructor throws this IllegalArgumentException to reject the malformed name early. Pulsar throws it because a topic without a local name can never resolve to a real topic.","triggerScenarios":"Calling TopicName.get() (or the constructor) with a name whose path ends right after the namespace, e.g. 'persistent://my-tenant/my-ns', 'persistent://my-tenant/my-ns/', or a name where the local segment is whitespace. Also produced by callers that build topic names by string concatenation and drop the topic part.","commonSituations":"Configuration files or environment variables with a truncated topic URL (copy/paste cut off); templates where a ${topic} placeholder was left empty; programmatic name building from split strings where the last element was lost; REST/client calls passing a namespace URL instead of a topic URL.","solutions":["Supply the full V2 topic name '<domain>://<tenant>/<namespace>/<local-name>', e.g. persistent://public/default/my-topic","Check the source config/env value for truncation or a missing final segment","If building names in code, validate the local segment is non-blank before calling TopicName.get()"],"exampleFix":"// before\nTopicName.get(\"persistent://public/default/\");\n// after\nTopicName.get(\"persistent://public/default/my-topic\");","handlingStrategy":"validation","validationCode":"public static void requireValidTopicUrl(String url) {\n    if (url == null || url.chars().filter(c -> c == ':').count() == 0) throw new IllegalArgumentException(\"missing domain scheme\");\n    String path = url.substring(url.indexOf(\"://\") + 3);\n    String[] parts = path.split(\"/\");\n    if (parts.length != 3 || parts[2].trim().isEmpty()) throw new IllegalArgumentException(\"local topic name must not be blank: \" + url);\n}","typeGuard":"public static boolean isCompleteTopicName(String s) {\n    if (s == null) return false;\n    int i = s.indexOf(\"://\");\n    if (i < 0) return false;\n    String[] parts = s.substring(i + 3).split(\"/\");\n    return parts.length == 3 && !parts[2].trim().isEmpty();\n}","tryCatchPattern":"try {\n    TopicName tn = TopicName.get(candidate);\n    // use tn\n} catch (IllegalArgumentException e) {\n    log.error(\"Rejected topic name '{}': {}\", candidate, e.getMessage());\n}","preventionTips":["Always use full V2 names: domain://tenant/namespace/topic","Never build topic URLs by naive string concatenation of possibly-empty variables","Validate with TopicName.isValid(topicName) or a split-segment check before calling TopicName.get","Watch for truncated config values when copying topic URLs"],"tags":["java","topic-naming","illegal-argument"],"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"}