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
- Pass only TransactionV5 instances (obtained via the v5 client's newTransaction flow) to unwrap().
- If holding a raw Transaction, route it through the v5 wrapper API instead of calling unwrap directly.
- 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
- Only pass transactions created through the v5 client API to unwrap()
- Do not mix pulsar-client v4 and v5 transaction objects in one flow
- Use the type guard before calling unwrap in shared/legacy code paths
- In tests, mock TransactionV5 rather than the base Transaction interface
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
- Invalid txnId key:
- Unsupported LongBitmap type: <other.getClass()>
- ${pd}${name} must be a ${cls.getName()}. (${field})
- Positions must not be null
- Invalid range ${range}
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/a37e8a24075a8d80.
Report an issue: GitHub.