{"id":"63fa35da5a6570fa","repo":"apache/kafka","slug":"cannot-send-offsets-if-a-transaction-is-not-in-pro","errorCode":null,"errorMessage":"Cannot send offsets if a transaction is not in progress (currentState= {currentState})","messagePattern":"Cannot send offsets if a transaction is not in progress \\(currentState= (.+?)\\)","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"clients/src/main/java/org/apache/kafka/clients/producer/internals/TransactionManager.java","lineNumber":439,"sourceCode":"        enqueueRequest(handler);\n\n        // If an epoch bump is required for recovery, initialize the transaction after completing the EndTxn request.\n        // If we are upgrading to TV2 transactions on the next transaction, also bump the epoch.\n        if (clientSideEpochBumpRequired) {\n            return initializeTransactions(this.producerIdAndEpoch);\n        }\n\n        return handler.result;\n    }\n\n    public synchronized TransactionalRequestResult sendOffsetsToTransaction(final Map<TopicPartition, OffsetAndMetadata> offsets,\n                                                                            final ConsumerGroupMetadata groupMetadata) {\n        ensureTransactional();\n        throwIfPendingState(TransactionOperation.SEND_OFFSETS_TO_TRANSACTION);\n        maybeFailWithError();\n\n        if (currentState != State.IN_TRANSACTION) {\n            throw new IllegalStateException(\"Cannot send offsets if a transaction is not in progress \" +\n                \"(currentState= \" + currentState + \")\");\n        }\n\n        // In transaction V2, the client will skip sending AddOffsetsToTxn before sending txnOffsetCommit.\n        TxnRequestHandler handler;\n        if (isTransactionV2Enabled()) {\n            log.debug(\"Begin adding offsets {} for consumer group {} to transaction with transaction protocol V2\", offsets, groupMetadata);\n            handler = txnOffsetCommitHandler(null, offsets, groupMetadata);\n            transactionStarted = true;\n        } else {\n            log.debug(\"Begin adding offsets {} for consumer group {} to transaction\", offsets, groupMetadata);\n            AddOffsetsToTxnRequest.Builder builder = new AddOffsetsToTxnRequest.Builder(\n                    new AddOffsetsToTxnRequestData()\n                            .setTransactionalId(transactionalId)\n                            .setProducerId(producerIdAndEpoch.producerId)\n                            .setProducerEpoch(producerIdAndEpoch.epoch)\n                            .setGroupId(groupMetadata.groupId())\n            );","sourceCodeStart":421,"sourceCodeEnd":457,"githubUrl":"https://github.com/apache/kafka/blob/c31c9215e131f8c17e79f8901b48c13ee6aa8e7a/clients/src/main/java/org/apache/kafka/clients/producer/internals/TransactionManager.java#L421-L457","documentation":"Thrown by TransactionManager.sendOffsetsToTransaction when the current state is anything other than IN_TRANSACTION. Sending consumer offsets to a transaction (the consume-transform-produce pattern) is only legal after beginTransaction() and before commit/abort; outside that window the coordinator would reject it, so the client rejects it up front with an IllegalStateException that leaves producer state intact.","triggerScenarios":"Calling producer.sendOffsetsToTransaction(offsets, groupMetadata) before beginTransaction(); after commitTransaction()/abortTransaction() has already completed; or in the READY state because initTransactions() finished but beginTransaction() was skipped. The state-machine guard at line 438-441 fires.","commonSituations":"Streams-style consume-produce code where the begin/commit bracket is missing or reordered; refactors that moved beginTransaction() into a conditional branch; recovering from an abort and forgetting to begin again before the next offset commit; unit tests that exercise the offset-send path without the full lifecycle.","solutions":["Call producer.beginTransaction() before producer.sendOffsetsToTransaction(...).","Ensure the offset send happens within the begin..commit bracket: initTransactions -> beginTransaction -> (send + sendOffsetsToTransaction) -> commitTransaction.","After an abort, start a fresh transaction (beginTransaction) before sending offsets again — the state is READY, not IN_TRANSACTION.","Add a state assertion helper in your code so mis-ordered lifecycle calls fail loudly in tests."],"exampleFix":"// before\nproducer.initTransactions();\nproducer.sendOffsetsToTransaction(offsets, groupMetadata); // throws\nproducer.commitTransaction();\n\n// after\nproducer.initTransactions();\nproducer.beginTransaction();\nproducer.sendOffsetsToTransaction(offsets, groupMetadata);\nproducer.commitTransaction();","handlingStrategy":"validation","validationCode":"// KafkaProducer hides transaction state; mirror it in your own variable.\nprivate volatile boolean inTransaction = false;\n// set true after beginTransaction(), false after commit/abort returns\n...\nif (!inTransaction) throw new IllegalStateException(\"sendOffsetsToTransaction requires an open transaction\");\nproducer.sendOffsetsToTransaction(offsets, groupMetadata);","typeGuard":null,"tryCatchPattern":"try {\n    producer.sendOffsetsToTransaction(offsets, groupMetadata);\n} catch (IllegalStateException ise) {\n    // transaction lifecycle out of sync; either beginTransaction first or skip this commit\n}","preventionTips":["Always call sendOffsetsToTransaction inside the [beginTransaction, commitTransaction] window.","Wrap consume-process-produce in a helper that pairs begin/commit/abort with explicit state tracking.","For exactly-once pipelines, gate on consumer group membership and transaction state before committing offsets."],"tags":["producer","transactions","eos","offsets","java"],"analyzedSha":"c31c9215e131f8c17e79f8901b48c13ee6aa8e7a","analyzedAt":"2026-08-03T12:34:05.770Z","schemaVersion":2}