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

  1. If you need to modify headers, do so before producer.send() takes ownership, or copy them: Headers copy = new RecordHeaders(orig.headers()).
  2. Move header stamping into the ProducerRecord construction site rather than an interceptor that runs after lock-down.
  3. When forwarding consumed records, build fresh ProducerRecords with new RecordHeaders(...) rather than reusing the consumed headers reference.
  4. 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

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


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