{"record":{"id":"05c478b8d7e77e44","repo":"apache/pulsar","slug":"topic-is-invalid-expected-format-domain","errorCode":null,"errorMessage":"${topic} is invalid. Expected format: '<domain>://tenant/namespace/topic'","messagePattern":"(.+?) is invalid\\. Expected format: '<domain>://tenant/namespace/topic'","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"pulsar-common/src/main/java/org/apache/pulsar/common/naming/TopicName.java","lineNumber":560,"sourceCode":"     * For convenience, clients can pass a short topic name:\n     * - \"<local-topic>\", which represents \"persistent://public/default/<local-topic>\"\n     * - \"<tenant>/<namespace>/<local-topic>\", which represents \"persistent://<tenant>/<namespace>/<local-topic>\"\n     *\n     * @param topic the topic name from client\n     * @return the full topic name.\n     */\n    public static String toFullTopicName(String topic) {\n        final int index = topic.indexOf(\"://\");\n        if (index >= 0) {\n            TopicDomain.getEnum(topic.substring(0, index));\n            final List<String> parts = splitBySlash(topic.substring(index + \"://\".length()), 4);\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: \" + topic);\n            }\n            if (parts.size() != 3) {\n                throw new IllegalArgumentException(topic + \" is invalid. \"\n                    + \"Expected format: '<domain>://tenant/namespace/topic'\");\n            }\n            NamespaceName.validateNamespaceName(parts.get(0), parts.get(1));\n            if (StringUtils.isBlank(parts.get(2))) {\n                throw new IllegalArgumentException(topic + \" has blank local topic\");\n            }\n            return topic; // it's a valid full topic name\n        } else {\n            List<String> parts = splitBySlash(topic, 0);\n            if (parts.size() != 1 && parts.size() != 3) {\n                throw new IllegalArgumentException(topic + \" is invalid\");\n            }\n            if (parts.size() == 1) {\n                if (StringUtils.isBlank(parts.get(0))) {\n                    throw new IllegalArgumentException(topic + \" has blank local topic\");\n                }\n                return \"persistent://public/default/\" + parts.get(0);\n            } else {","sourceCodeStart":542,"sourceCodeEnd":578,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-common/src/main/java/org/apache/pulsar/common/naming/TopicName.java#L542-L578","documentation":"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.","triggerScenarios":"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'.","commonSituations":"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.","solutions":["Use exactly '<domain>://<tenant>/<namespace>/<local-topic>' with 3 slash-separated parts after '://'","Count the segments in your input; fix missing or extra segments","For short names, omit '://' entirely and let Pulsar apply the default (see short-name handling)"],"exampleFix":"// before\nTopicName.toFullTopicName(\"persistent://my-tenant/my-ns\");\n// after\nTopicName.toFullTopicName(\"persistent://my-tenant/my-ns/my-topic\");","handlingStrategy":"validation","validationCode":"public static boolean isWellFormedTopicUrl(String s) {\n    if (s == null) return false;\n    int i = s.indexOf(\"://\");\n    if (i < 0) return false;\n    return s.substring(i + 3).split(\"/\").length == 3;\n}","typeGuard":null,"tryCatchPattern":"try {\n    String full = TopicName.toFullTopicName(raw);\n} catch (IllegalArgumentException e) {\n    log.error(\"Topic '{}' does not match '<domain>://tenant/namespace/topic': {}\", raw, e.getMessage());\n}","preventionTips":["Count segments after '://' — it must be exactly 3 for a valid V2 URL","Don't confuse REST admin paths (/admin/v2/persistent/tenant/ns/topic) with topic URLs","Build names with TopicName objects or fromPersistenceNamingEncoding rather than string surgery"],"tags":["java","topic-naming","format-validation"],"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"}