apache/kafka · error · IllegalStateException
RecordHeaders has been closed.
Error message
RecordHeaders has been closed.
What it means
Thrown as IllegalStateException by RecordHeaders.canWrite() when an attempt is made to mutate a RecordHeaders whose isReadOnly flag has been set (via setReadOnly()). Kafka marks headers read-only once a record has been handed to the producer/internals, so further mutation would corrupt already-serialized state. The check guards add(Header), add(key,value), remove(key), and iterator().remove() on the closeAware wrapper.
Source
Thrown at clients/src/main/java/org/apache/kafka/common/header/internals/RecordHeaders.java:131
this.isReadOnly = true;
}
public boolean isReadOnly() {
return isReadOnly;
}
public Header[] toArray() {
return headers.isEmpty() ? Record.EMPTY_HEADERS : headers.toArray(new Header[0]);
}
private void checkKey(String key) {
if (key == null)
throw new IllegalArgumentException("key cannot be null.");
}
private void canWrite() {
if (isReadOnly)
throw new IllegalStateException("RecordHeaders has been closed.");
}
private Iterator<Header> closeAware(final Iterator<Header> original) {
return new Iterator<>() {
@Override
public boolean hasNext() {
return original.hasNext();
}
public Header next() {
return original.next();
}
@Override
public void remove() {
canWrite();
original.remove();
}View on GitHub (pinned to c31c9215e1)
Solutions
- If you need to modify headers, do so before producer.send() takes ownership, or copy them: Headers copy = new RecordHeaders(orig.headers()).
- Move header stamping into the ProducerRecord construction site rather than an interceptor that runs after lock-down.
- When forwarding consumed records, build fresh ProducerRecords with new RecordHeaders(...) rather than reusing the consumed headers reference.
- If writing an interceptor, treat the supplied record/headers as immutable and return a new copy with the changes.
Example fix
// before: mutates headers after send
producer.send(record, (meta, e) -> record.headers().add("sent", b));
// after: copy before mutating, or stamp before send
Headers h = new RecordHeaders(record.headers());
h.add("sent", b);
producer.send(new ProducerRecord<>(record.topic(), record.partition(), record.key(), record.value(), h)); Defensive patterns
Strategy: validation
Validate before calling
if (headers.isReadOnly()) {
// headers have been frozen (e.g. by the producer after send);
// create a fresh RecordHeaders copy before mutating
headers = new org.apache.kafka.common.header.internals.RecordHeaders(headers.toArray());
}
headers.add(newKey, newValue); Type guard
static boolean isMutable(org.apache.kafka.common.header.Headers h) {
return !(h instanceof org.apache.kafka.common.header.internals.RecordHeaders)
|| !((org.apache.kafka.common.header.internals.RecordHeaders) h).isReadOnly();
} Try / catch
try {
headers.add(header);
} catch (IllegalStateException e) {
// message: "RecordHeaders has been closed."
// the producer has taken ownership; copy into a new RecordHeaders and retry
} Prevention
- Do not mutate a Headers instance after passing it to Producer.send() — the producer freezes it
- Build the complete header set before send() rather than appending later
- When you need to modify headers received from a ConsumerRecord, copy them first via new RecordHeaders(source)
- Check Headers is not read-only in shared utility code that mutates headers
- Avoid reusing a single RecordHeaders across multiple ProducerRecord instances
When it happens
Trigger: Calling headers.add(...), headers.remove(...), or iterator().remove() on a Headers instance returned from a ProducerRecord that has already been submitted, or on headers exposed by a consumed Record (the consumer exposes headers as read-only). Also triggered by an interceptor/serializer mutating the headers argument after the producer has taken ownership.
Common situations: ProducerInterceptor.onSend mutates headers after they are already locked; a serializer tries to stamp a header on a record mid-send; application code retains a reference to a Headers object and edits it after producer.send returns; consuming a record and reusing its headers object directly in a new ProducerRecord without copying.
Related errors
- Headers cannot be null
- This consumer has already been closed.
- Consumer is not subscribed to any topics or assigned any par
- This RebalanceConsumer is already closed. Re-use of this obj
- Attempt to retrieve value from future which hasn't successfu
AI-assisted analysis of apache/kafka@c31c9215e1 (2026-08-03).
Data as JSON: /data/errors/3129e6e6d811061d.json.
Report an issue: GitHub.