{"record":{"id":"5a40ee91ed9cc5fd","repo":"apache/pulsar","slug":"cannot-terminate-non-persistent-topic-segmentto","errorCode":null,"errorMessage":"Cannot terminate non-persistent topic: ${segmentTopic}","messagePattern":"Cannot terminate non-persistent topic: (.+?)","errorType":"http","errorClass":"RestException","httpStatus":400,"severity":"error","filePath":"pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Segments.java","lineNumber":169,"sourceCode":"            @Parameter(description = \"Segment descriptor (e.g. 0000-7fff-1)\", required = true)\n            @PathParam(\"descriptor\") String descriptor,\n            @Parameter(description = \"Whether leader broker redirected this call to this broker.\")\n            @QueryParam(\"authoritative\") @DefaultValue(\"false\") boolean authoritative) {\n        validateNamespaceName(tenant, namespace);\n        TopicName segmentTopic = segmentTopicName(tenant, namespace, encodedTopic, descriptor);\n\n        validateSuperUserAccessAsync()\n                .thenCompose(__ -> validateTopicOwnershipAsync(segmentTopic, authoritative))\n                .thenCompose(__ -> pulsar().getBrokerService().getTopicIfExists(segmentTopic.toString()))\n                .thenCompose(optTopic -> {\n                    if (optTopic.isEmpty()) {\n                        throw new RestException(Response.Status.NOT_FOUND,\n                                \"Segment topic not found: \" + segmentTopic);\n                    }\n                    if (optTopic.get() instanceof PersistentTopic pt) {\n                        return pt.terminate().thenApply(__ -> null);\n                    }\n                    throw new RestException(Response.Status.BAD_REQUEST,\n                            \"Cannot terminate non-persistent topic: \" + segmentTopic);\n                })\n                .thenAccept(__ -> {\n                    log.info().attr(\"clientAppId\", clientAppId()).attr(\"segment\", segmentTopic)\n                            .log(\"Terminated segment topic\");\n                    asyncResponse.resume(Response.noContent().build());\n                })\n                .exceptionally(ex -> {\n                    log.error().attr(\"clientAppId\", clientAppId()).attr(\"segment\", segmentTopic)\n                            .exception(ex).log(\"Failed to terminate segment topic\");\n                    resumeAsyncResponseExceptionally(asyncResponse, ex);\n                    return null;\n                });\n    }\n\n    @PUT\n    @Path(\"/{tenant}/{namespace}/{topic}/{descriptor}/subscription/{subscription}\")\n    @Operation(summary = \"Create a subscription cursor on the segment topic at the earliest\"","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/Segments.java#L151-L187","documentation":"HTTP 400 thrown by terminateSegment when the loaded topic instance is not a PersistentTopic. Termination (sealing the ledger so no more writes are accepted) is only defined for persistent topics, so a non-persistent instance is rejected as a bad request.","triggerScenarios":"Calling the segment terminate endpoint against a segment topic that materialized as a non-persistent topic — e.g. the segment name maps to a non-persistent domain topic or a misconfigured topic domain, so the broker loaded a NonPersistentTopic instead.","commonSituations":"Constructing the segment topic name with the wrong domain prefix (non-persistent:// instead of persistent://); namespace-level policies or custom pluggable topic types yielding a different Topic implementation; test setups using non-persistent topics interchangeably.","solutions":["Ensure the segment topic is addressed with the persistent:// domain — segment topics are always persistent.","Fix the code building the segment topic name (SegmentTopicName.fromParent) or the URL path so it targets the persistent topic.","If you need to stop writes on a non-persistent topic, disconnect producers instead — terminate does not apply."],"exampleFix":"// before\nTopicName seg = TopicName.get(\"non-persistent\", ns, localName); // 400\n// after\nTopicName seg = TopicName.get(\"persistent\", ns, localName);","handlingStrategy":"validation","validationCode":"if (!segmentTopic.toString().startsWith('persistent://')) {\n  throw new Error('segment topics must use the persistent:// domain');\n}","typeGuard":"const isPersistentTopicName = (tn) => typeof tn === 'string' && tn.startsWith('persistent://');","tryCatchPattern":"try {\n  await admin.scalableTopics().terminateSegment(segmentTopic);\n} catch (e) {\n  if (e.status === 400 && /non-persistent/.test(e.message)) { /* fix domain prefix */ }\n  throw e;\n}","preventionTips":["Always compose segment names via SegmentTopicName.fromParent with the persistent domain.","Never reuse non-persistent topic handles for segment operations.","Add a unit check on segment-name construction in your tooling."],"tags":["rest","bad-request","termination","segment","http-400"],"backgroundTag":"unsupported-topic-type","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"}