{"id":"3129e6e6d811061d","repo":"apache/kafka","slug":"recordheaders-has-been-closed","errorCode":null,"errorMessage":"RecordHeaders has been closed.","messagePattern":"RecordHeaders has been closed\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"clients/src/main/java/org/apache/kafka/common/header/internals/RecordHeaders.java","lineNumber":131,"sourceCode":"        this.isReadOnly = true;\n    }\n\n    public boolean isReadOnly() {\n        return isReadOnly;\n    }\n\n    public Header[] toArray() {\n        return headers.isEmpty() ? Record.EMPTY_HEADERS : headers.toArray(new Header[0]);     \n    }\n\n    private void checkKey(String key) {\n        if (key == null)\n            throw new IllegalArgumentException(\"key cannot be null.\");\n    }\n\n    private void canWrite() {\n        if (isReadOnly)\n            throw new IllegalStateException(\"RecordHeaders has been closed.\");\n    }\n\n    private Iterator<Header> closeAware(final Iterator<Header> original) {\n        return new Iterator<>() {\n            @Override\n            public boolean hasNext() {\n                return original.hasNext();\n            }\n\n            public Header next() {\n                return original.next();\n            }\n\n            @Override\n            public void remove() {\n                canWrite();\n                original.remove();\n            }","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/apache/kafka/blob/c31c9215e131f8c17e79f8901b48c13ee6aa8e7a/clients/src/main/java/org/apache/kafka/common/header/internals/RecordHeaders.java#L113-L149","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: mutates headers after send\nproducer.send(record, (meta, e) -> record.headers().add(\"sent\", b));\n\n// after: copy before mutating, or stamp before send\nHeaders h = new RecordHeaders(record.headers());\nh.add(\"sent\", b);\nproducer.send(new ProducerRecord<>(record.topic(), record.partition(), record.key(), record.value(), h));","handlingStrategy":"validation","validationCode":"if (headers.isReadOnly()) {\n    // headers have been frozen (e.g. by the producer after send);\n    // create a fresh RecordHeaders copy before mutating\n    headers = new org.apache.kafka.common.header.internals.RecordHeaders(headers.toArray());\n}\nheaders.add(newKey, newValue);","typeGuard":"static boolean isMutable(org.apache.kafka.common.header.Headers h) {\n    return !(h instanceof org.apache.kafka.common.header.internals.RecordHeaders)\n        || !((org.apache.kafka.common.header.internals.RecordHeaders) h).isReadOnly();\n}","tryCatchPattern":"try {\n    headers.add(header);\n} catch (IllegalStateException e) {\n    // message: \"RecordHeaders has been closed.\"\n    // the producer has taken ownership; copy into a new RecordHeaders and retry\n}","preventionTips":["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"],"tags":["headers","producer","consumer","illegal-state","immutability"],"analyzedSha":"c31c9215e131f8c17e79f8901b48c13ee6aa8e7a","analyzedAt":"2026-08-03T12:34:05.770Z","schemaVersion":2}