apache/pulsar · error · RestException
Create subscription on non-persistent topic can only be…
Error message
Create subscription on non-persistent topic can only be done through client
What it means
createSubscription only works for persistent topics; for non-persistent topics subscriptions must be created through the client (they are ephemeral and not managed via cursors in the metadata store), so the admin endpoint returns HTTP 400.
Solutions
- Create the subscription by connecting a consumer/reader client with that subscription name to the non-persistent topic
- Switch to the persistent topic endpoint only for persistent topics
- Reconsider topic persistence if durable admin-managed subscriptions are required
Example fix
// before
admin.topics().createSubscription("non-persistent://public/default/t", "sub", MessageId.earliest);
// after
try (Consumer<byte[]> c = client.newConsumer().topic("non-persistent://public/default/t")
.subscriptionName("sub").subscriptionInitialPosition(InitialPosition.Earliest).subscribe()) {} Defensive patterns
Strategy: validation
Validate before calling
if (topic.startsWith("non-persistent://")) { createSubscriptionViaClient(topic, sub); return; } Type guard
boolean isAdminSubscriptionCreationAllowed(String t) { return t != null && t.startsWith("persistent://"); } Try / catch
try { admin.topics().createSubscription(topic, sub, pos); } catch (PulsarAdminException e) { if (e.getStatusCode() == 400) { /* fall back to client consumer creation */ } } Prevention
- Branch on topic persistence before calling the admin subscription API
- Create non-persistent subscriptions with a consumer client
- Use readers/consumers with InitialPosition.Earliest as the fallback
When it happens
Trigger: PUT /admin/v2/{tenant}/{namespace}/{topic}/subscription/{subName} (non-persistent endpoint variant) with topicName.isPersistent() == false.
Common situations: Generic admin scripts that use the same subscription-creation code path for all topics; migrations from persistent to non-persistent topics; using PulsarAdmin against a non-persistent topic URL.
Related errors
- getSubNotFoundErrorMessage(topicName.toString(), subName)
- Namespace does not exist
- Please let the subscription only contains '/w(a-zA-Z_0-9)'…
- Subscription has active connected consumers
- Subscription not found
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/7e0c1c6d6df64bb9.
Report an issue: GitHub.
Appendix: source
Thrown at pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/v2/PersistentTopics.java:1989
ResetCursorData resetCursorData,
@Parameter(description = "Is replicated required to perform this operation")
@QueryParam("replicated") boolean replicated
) {
try {
validateTopicName(tenant, namespace, topic);
String decodedSubName = decode(encodedSubName);
// If subscription is as "a/b". The url of HTTP API that defined as
// "{tenant}/{namespace}/{topic}/{subscription}" will be like below:
// "public/default/tp/a/b", then the broker will assume it is a topic that
// using the old rule "{tenant}/{cluster}/{namespace}/{topic}/{subscription}".
// So denied to create a subscription that contains "/".
if (pulsar().getConfig().isStrictlyVerifySubscriptionName()
&& !NamedEntity.isAllowed(decodedSubName)) {
throw new RestException(Response.Status.BAD_REQUEST, "Please let the subscription only contains"
+ " '/w(a-zA-Z_0-9)' or '_', the current value is " + decodedSubName);
}
if (!topicName.isPersistent()) {
throw new RestException(Response.Status.BAD_REQUEST, "Create subscription on non-persistent topic "
+ "can only be done through client");
}
Map<String, String> subscriptionProperties = resetCursorData == null ? null :
resetCursorData.getProperties();
MessageIdImpl messageId = resetCursorData == null ? null :
new MessageIdImpl(resetCursorData.getLedgerId(), resetCursorData.getEntryId(),
resetCursorData.getPartitionIndex());
internalCreateSubscription(asyncResponse, decode(encodedSubName), messageId, authoritative,
replicated, subscriptionProperties);
} catch (WebApplicationException wae) {
asyncResponse.resume(wae);
} catch (Exception e) {
asyncResponse.resume(new RestException(e));
}
}
@POST
@Path("/{tenant}/{namespace}/{topic}/subscription/{subName}/resetcursor/{timestamp}")View on GitHub (pinned to 820761864e)