apache/pulsar · error · IllegalArgumentException

Expected TransactionV5, got: + txn.getClass()

Error message

Expected TransactionV5, got: + txn.getClass()

What it means

TransactionV5.unwrap() downcasts an opaque org.apache.pulsar.client.api.transaction.Transaction to the internal TransactionV5 to access the underlying v4 transaction. If the object is not a TransactionV5 instance (and is not null), it throws IllegalArgumentException with the concrete class name. Passing a v4-client transaction or a foreign Transaction implementation triggers this.

Source

Thrown at pulsar-client-v5/src/main/java/org/apache/pulsar/client/impl/v5/TransactionV5.java:98

            case COMMITTING -> State.COMMITTING;
            case ABORTING -> State.ABORTING;
            case COMMITTED -> State.COMMITTED;
            case ABORTED -> State.ABORTED;
            case ERROR -> State.ERROR;
            case TIME_OUT -> State.TIMED_OUT;
        };
    }

    /**
     * Unwrap the v4 transaction from a V5 handle, validating type. Returns null when
     * the handle is null (no transaction context).
     */
    static org.apache.pulsar.client.api.transaction.Transaction unwrap(Transaction txn) {
        if (txn == null) {
            return null;
        }
        if (!(txn instanceof TransactionV5 v5)) {
            throw new IllegalArgumentException("Expected TransactionV5, got: " + txn.getClass());
        }
        return v5.v4Transaction;
    }

    private final class AsyncView implements AsyncTransaction {
        @Override
        public CompletableFuture<Void> commit() {
            return v4Transaction.commit();
        }

        @Override
        public CompletableFuture<Void> abort() {
            return v4Transaction.abort();
        }
    }
}

View on GitHub (pinned to 820761864e)

Solutions

  1. Pass only TransactionV5 instances (obtained via the v5 client's newTransaction flow) to unwrap().
  2. If holding a raw Transaction, route it through the v5 wrapper API instead of calling unwrap directly.
  3. Fix test mocks to wrap the v4 transaction the same way TransactionV5 does, or mock TransactionV5 itself.

Example fix

// before
org.apache.pulsar.client.api.transaction.Transaction txn = pulsarClient.newTransaction();
var inner = TransactionV5.unwrap(txn); // IllegalArgumentException
// after
AsyncTransaction txn = streamClient.newTransaction();
var inner = TransactionV5.unwrap((TransactionV5) txn.rawTransaction());
Defensive patterns

Strategy: type-guard

Validate before calling

if (!(txn instanceof TransactionV5)) {
    throw new IllegalArgumentException("unwrap requires a TransactionV5, got " + txn.getClass());
}

Type guard

static TransactionV5 asV5(Transaction txn) {
    return (txn instanceof TransactionV5 v5) ? v5 : null;
}

Try / catch

try {
    var inner = TransactionV5.unwrap(txn);
} catch (IllegalArgumentException e) {
    log.error("wrong transaction type: {}", e.getMessage());
}

Prevention

When it happens

Trigger: Calling unwrap() (package-internal helper) with a Transaction obtained from the v4 client API, a mock/stub, or any custom Transaction implementation instead of the v5 wrapper created by TransactionV5 flows.

Common situations: Mixing pulsar-client v4 and v5 APIs in one application; test doubles that implement the Transaction interface; refactoring that replaced a v5 transaction with a base-API transaction before calling internal helpers.

Related errors


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