{"id":"982a58672a930bf3","repo":"apache/kafka","slug":"invalid-control-record-type-for-end-transaction-ma","errorCode":null,"errorMessage":"Invalid control record type for end transaction marker {}","messagePattern":"Invalid control record type for end transaction marker (.+?)","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"clients/src/main/java/org/apache/kafka/common/record/internal/EndTransactionMarker.java","lineNumber":79,"sourceCode":"    @Override\n    public boolean equals(Object o) {\n        if (this == o) return true;\n        if (o == null || getClass() != o.getClass()) return false;\n\n        EndTransactionMarker that = (EndTransactionMarker) o;\n        return coordinatorEpoch == that.coordinatorEpoch && type == that.type;\n    }\n\n    @Override\n    public int hashCode() {\n        int result = type != null ? type.hashCode() : 0;\n        result = 31 * result + coordinatorEpoch;\n        return result;\n    }\n\n    private static void ensureTransactionMarkerControlType(ControlRecordType type) {\n        if (type != ControlRecordType.COMMIT && type != ControlRecordType.ABORT)\n            throw new IllegalArgumentException(\"Invalid control record type for end transaction marker \" + type);\n    }\n\n    public static EndTransactionMarker deserialize(Record record) {\n        ControlRecordType type = ControlRecordType.parse(record.key());\n        return deserializeValue(type, record.value());\n    }\n\n    // Visible for testing\n    static EndTransactionMarker deserializeValue(ControlRecordType type, ByteBuffer value) {\n        ensureTransactionMarkerControlType(type);\n\n        short version = value.getShort();\n        if (version < EndTxnMarker.LOWEST_SUPPORTED_VERSION)\n            throw new InvalidRecordException(\"Invalid version found for end transaction marker: \" + version +\n                    \". May indicate data corruption\");\n\n        if (version > EndTxnMarker.HIGHEST_SUPPORTED_VERSION) {\n            log.debug(\"Received end transaction marker value version {}. Parsing as version {}\", version,","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/apache/kafka/blob/c31c9215e131f8c17e79f8901b48c13ee6aa8e7a/clients/src/main/java/org/apache/kafka/common/record/internal/EndTransactionMarker.java#L61-L97","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","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.","If caused by corruption, recover from ISR or truncate the segment.","If caused by version skew, upgrade the broker/client to a version that understands the control record types in use."],"exampleFix":"// before: blindly deserializing every control record as end-txn\nfor (RecordBatch b : records) {\n    if (b.isControlBatch())\n        EndTransactionMarker.deserialize(b.iterator().next());\n}\n// after: check the control type before decoding\nfor (RecordBatch b : records) {\n    if (!b.isControlBatch()) continue;\n    Record r = b.iterator().next();\n    ControlRecordType t = ControlRecordType.parse(r.key());\n    if (t == ControlRecordType.COMMIT || t == ControlRecordType.ABORT)\n        EndTransactionMarker.deserialize(r);\n}","handlingStrategy":"validation","validationCode":"// Validate BEFORE constructing EndTransactionMarker:\nimport org.apache.kafka.common.record.ControlRecordType;\n\nControlRecordType type = /* resolved from control record key */;\nif (type != ControlRecordType.COMMIT && type != ControlRecordType.ABORT) {\n    throw new IllegalArgumentException(\n        \"EndTransactionMarker requires COMMIT or ABORT, got: \" + type);\n}\nreturn new EndTransactionMarker(type, coordinatorEpoch);","typeGuard":"// Narrow ControlRecordType to the only two legal end-txn marker kinds.\nstatic boolean isEndTxnMarkerType(org.apache.kafka.common.record.ControlRecordType t) {\n    return t == org.apache.kafka.common.record.ControlRecordType.COMMIT\n        || t == org.apache.kafka.common.record.ControlRecordType.ABORT;\n}","tryCatchPattern":null,"preventionTips":["Only COMMIT and ABORT are legal end-transaction marker types — ABORT marker (and COMMIT) are the entire valid set; filter at the boundary.","When parsing control records, skip/filter unknown ControlRecordType values rather than passing them to EndTransactionMarker construction.","Validate ControlRecordType.parse(key) output before use; a corrupt key can deserialize to an unexpected enum.","Keep this check at the ingestion boundary (one place) so downstream code can assume the type is already constrained.","Unit-test the boundary guard against every ControlRecordType enum value to lock in the COMMIT/ABORT allow-list."],"tags":["kafka","records","transactions","control-record","end-txn"],"analyzedSha":"c31c9215e131f8c17e79f8901b48c13ee6aa8e7a","analyzedAt":"2026-08-03T12:34:05.770Z","schemaVersion":2}