apache/pulsar · warning · RestException

GetMessageById is not allowed on partitioned-topic

Error message

GetMessageById is not allowed on partitioned-topic

What it means

The getMessageById admin REST operation refuses to run on a partitioned topic's base name. A message ID (ledgerId/entryId) is only meaningful within a single persistent backing topic (a partition), so the broker cannot look up a message by ID across all partitions. It is raised as 405 METHOD_NOT_ALLOWED after the partitioned-topic metadata check.

Source

Thrown at pulsar-broker/src/main/java/org/apache/pulsar/broker/admin/impl/PersistentTopicsBase.java:2951

            seekPosition = isExcluded ? seekPosition.getNext() : seekPosition;
        }
        return seekPosition;
    }

    protected CompletableFuture<Response> internalGetMessageById(long ledgerId, long entryId, boolean authoritative) {
        return validateTopicOperationAsync(topicName, TopicOperation.PEEK_MESSAGES)
        .thenCompose(__ -> validateGlobalNamespaceOwnershipAsync(namespaceName))
        .thenCompose(__ -> {
            if (topicName.isPartitioned()) {
                return CompletableFuture.completedFuture(null);
            } else {
                return getPartitionedTopicMetadataAsync(topicName, authoritative, false)
                        .thenAccept(topicMetadata -> {
                            if (topicMetadata.partitions > 0) {
                                log.warn()
                                        .attr("topic", topicName)
                                        .log("Not supported getMessageById operation on partitioned-topic");
                                throw new RestException(Status.METHOD_NOT_ALLOWED,
                                        "GetMessageById is not allowed on partitioned-topic");
                            }
                        });
            }
        }).thenCompose(ignore -> validateTopicOwnershipAsync(topicName, authoritative))
        .thenCompose(__ -> getTopicReferenceAsync(topicName))
        .thenCompose(topic -> {
            CompletableFuture<Response> results = new CompletableFuture<>();
            ManagedLedger ledger = ((PersistentTopic) topic).getManagedLedger();
            ledger.asyncReadEntry(PositionFactory.create(ledgerId, entryId),
                    new AsyncCallbacks.ReadEntryCallback() {
                        @Override
                        public void readEntryFailed(ManagedLedgerException exception,
                                                    Object ctx) {
                            if (exception instanceof ManagedLedgerException.LedgerNotExistException) {
                                results.completeExceptionally(
                                        new RestException(Status.NOT_FOUND, "Message id not found"));
                                return;

View on GitHub (pinned to 820761864e)

Solutions

  1. Determine which partition owns the ledger and call the endpoint with that partition name (my-topic-partition-N)
  2. If the partition is unknown, iterate all partitions and try getMessageById on each
  3. Avoid the ambiguity by persisting the full partition topic name alongside any message ID

Example fix

// before
String msg = admin.topics().getMessageById("persistent://public/default/my-topic", ledgerId, entryId);
// after
String msg = admin.topics().getMessageById("persistent://public/default/my-topic-partition-0", ledgerId, entryId);
Defensive patterns

Strategy: validation

Validate before calling

PartitionedTopicMetadata md = admin.topics().getPartitionedMetadata(topic);
if (md.partitions > 0) throw new IllegalArgumentException("getMessageById requires a partition topic name");

Type guard

boolean isPartitionName(String topic) { return topic.matches(".*-partition-\\d+$"); }

Try / catch

try { admin.topics().getMessageById(topic, ledgerId, entryId); }
catch (PulsarAdminException e) {
    if (e.getStatusCode() == 405) { /* retry on partitions */ }
}

Prevention

When it happens

Trigger: GET /admin/v2/persistent/{tenant}/{namespace}/{topic}/ledger/{ledgerId}/entry/{entryId} with the partitioned topic's base name; the metadata check sees partitions > 0 and throws before ownership validation.

Common situations: Copying a ledgerId/entryId from broker logs and trying to fetch it via the partitioned topic name; debugging tools that accept a topic URL but don't resolve partitions.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/3ad4da8f826e7cac. Report an issue: GitHub.