apache/kafka · error · IllegalArgumentException

Invalid control record type for end transaction marker {}

Error message

Invalid control record type for end transaction marker {}

What it means

Thrown by EndTransactionMarker.ensureTransactionMarkerControlType() (also called from deserializeValue) when the control record type parsed from a transaction marker record is neither COMMIT nor ABORT. EndTransactionMarker is specifically the record value that terminates a transaction, so only those two control types are legal; any other (or UNKNOWN) indicates the record is not actually an end-txn marker or the key was corrupted/mis-parsed. It is an IllegalArgumentException because the input violates the method's precondition rather than indicating I/O failure.

Source

Thrown at clients/src/main/java/org/apache/kafka/common/record/internal/EndTransactionMarker.java:79

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        EndTransactionMarker that = (EndTransactionMarker) o;
        return coordinatorEpoch == that.coordinatorEpoch && type == that.type;
    }

    @Override
    public int hashCode() {
        int result = type != null ? type.hashCode() : 0;
        result = 31 * result + coordinatorEpoch;
        return result;
    }

    private static void ensureTransactionMarkerControlType(ControlRecordType type) {
        if (type != ControlRecordType.COMMIT && type != ControlRecordType.ABORT)
            throw new IllegalArgumentException("Invalid control record type for end transaction marker " + type);
    }

    public static EndTransactionMarker deserialize(Record record) {
        ControlRecordType type = ControlRecordType.parse(record.key());
        return deserializeValue(type, record.value());
    }

    // Visible for testing
    static EndTransactionMarker deserializeValue(ControlRecordType type, ByteBuffer value) {
        ensureTransactionMarkerControlType(type);

        short version = value.getShort();
        if (version < EndTxnMarker.LOWEST_SUPPORTED_VERSION)
            throw new InvalidRecordException("Invalid version found for end transaction marker: " + version +
                    ". May indicate data corruption");

        if (version > EndTxnMarker.HIGHEST_SUPPORTED_VERSION) {
            log.debug("Received end transaction marker value version {}. Parsing as version {}", version,

View on GitHub (pinned to c31c9215e1)

Solutions

  1. Inspect the control record's key with kafka-dump-log.sh --deep-iteration to confirm the ControlRecordType; verify the key length and version byte are the expected 4 bytes.
  2. If the record is a non-txn control type being routed through end-txn decoding, fix the caller to only call EndTransactionMarker.deserialize on batches whose type is COMMIT/ABORT.
  3. If caused by corruption, recover from ISR or truncate the segment.
  4. If caused by version skew, upgrade the broker/client to a version that understands the control record types in use.

Example fix

// before: blindly deserializing every control record as end-txn
for (RecordBatch b : records) {
    if (b.isControlBatch())
        EndTransactionMarker.deserialize(b.iterator().next());
}
// after: check the control type before decoding
for (RecordBatch b : records) {
    if (!b.isControlBatch()) continue;
    Record r = b.iterator().next();
    ControlRecordType t = ControlRecordType.parse(r.key());
    if (t == ControlRecordType.COMMIT || t == ControlRecordType.ABORT)
        EndTransactionMarker.deserialize(r);
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate BEFORE constructing EndTransactionMarker:
import org.apache.kafka.common.record.ControlRecordType;

ControlRecordType type = /* resolved from control record key */;
if (type != ControlRecordType.COMMIT && type != ControlRecordType.ABORT) {
    throw new IllegalArgumentException(
        "EndTransactionMarker requires COMMIT or ABORT, got: " + type);
}
return new EndTransactionMarker(type, coordinatorEpoch);

Type guard

// Narrow ControlRecordType to the only two legal end-txn marker kinds.
static boolean isEndTxnMarkerType(org.apache.kafka.common.record.ControlRecordType t) {
    return t == org.apache.kafka.common.record.ControlRecordType.COMMIT
        || t == org.apache.kafka.common.record.ControlRecordType.ABORT;
}

Prevention

When it happens

Trigger: Calling EndTransactionMarker.deserialize(record) or constructing new EndTransactionMarker(type, epoch) with a ControlRecordType other than COMMIT/ABORT. Concretely: ControlRecordType.parse(record.key()) returns ABORT_MARKER/COMMIT_MARKER for valid data, but returns UNKNOWN or another type for an unexpected key, then deserializeValue calls ensureTransactionMarkerControlType and throws.

Common situations: Reading a control record that is actually a different control type (e.g. a Raft leader-change or snapshot records interpreted as end-txn), a transactional producer writing a malformed control record key, log corruption that scrambled the control record key bytes, or a client/broker version skew where new control record types exist that this older build does not recognize as transactional. Can also arise from incorrect manual decoding of control batches.

Related errors


AI-assisted analysis of apache/kafka@c31c9215e1 (2026-08-03). Data as JSON: /data/errors/982a58672a930bf3.json. Report an issue: GitHub.