apache/pulsar · error · java.lang.IllegalArgumentException

Expected MessageIdV5, got: ${messageId.getClass()}

Error message

Expected MessageIdV5, got: ${messageId.getClass()}

What it means

Thrown by MultiTopicQueueConsumer.routeAck when the acked MessageId is not a MessageIdV5 instance. Multi-topic routing requires the id to carry per-topic metadata that only MessageIdV5 exposes; any other MessageId implementation cannot be routed to the correct underlying topic consumer.

Source

Thrown at pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/MultiTopicQueueConsumer.java:331

    @Override
    public void acknowledge(MessageId messageId) {
        routeAck(messageId, ptc -> ptc.acknowledge(messageId));
    }

    @Override
    public void acknowledge(MessageId messageId, Transaction txn) {
        routeAck(messageId, ptc -> ptc.acknowledge(messageId, txn));
    }

    @Override
    public void negativeAcknowledge(MessageId messageId) {
        routeAck(messageId, ptc -> ptc.negativeAcknowledge(messageId));
    }

    /** Look up the per-topic consumer via the parent topic tag and delegate. */
    private void routeAck(MessageId messageId, java.util.function.Consumer<QueueConsumer<T>> action) {
        if (!(messageId instanceof MessageIdV5 id)) {
            throw new IllegalArgumentException("Expected MessageIdV5, got: " + messageId.getClass());
        }
        String parent = id.parentTopic();
        if (parent == null) {
            throw new IllegalStateException("MessageIdV5 missing parent topic — was the message"
                    + " delivered through a multi-topic consumer?");
        }
        PerTopicState<T> state = perTopic.get(parent);
        if (state == null) {
            // Topic was removed between deliver and ack. Fine — broker has dropped the
            // session for that topic. Drop the ack silently.
            log.debug().attr("topic", parent)
                    .log("Ack for removed topic; dropping");
            return;
        }
        action.accept(state.consumer);
    }

    @Override

View on GitHub (pinned to 820761864e)

Solutions

  1. Ack only with MessageId instances returned by the same multi-topic consumer's receive()
  2. Check messageId instanceof MessageIdV5 before passing it to acknowledge/negativeAcknowledge
  3. If migrating from another client version, re-receive the message rather than reusing old ids

Example fix

// before
consumer.acknowledge(oldMessageId); // MessageIdV3 from another consumer
// after
if (oldMessageId instanceof MessageIdV5 id) {
    consumer.acknowledge(id);
}
Defensive patterns

Strategy: type-guard

Type guard

static boolean isAckable(MessageId id) { return id instanceof MessageIdV5; }

Try / catch

try { consumer.acknowledge(id); } catch (IllegalArgumentException e) { log.error("wrong id type for multi-topic ack", e); }

Prevention

When it happens

Trigger: Calling acknowledge(messageId) or negativeAcknowledge(messageId) on a MultiTopicQueueConsumer with a MessageId obtained from a different consumer type (single-topic consumer, MessageIdV3, or a deserialized id).

Common situations: Mixing message ids between a regular topic consumer and a multi-topic consumer; replaying ids persisted from an older client version; constructing an id manually.

Related errors


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