This is an automated email from the ASF dual-hosted git repository.

pvillard31 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 4776001c55e NIFI-16026 - Add Hex header encoding option to 
ConsumeKafka (#11356)
4776001c55e is described below

commit 4776001c55e1657c0e81935711cff9f05f636eb1
Author: Alaksiej Ščarbaty <[email protected]>
AuthorDate: Mon Jun 22 11:13:37 2026 +0200

    NIFI-16026 - Add Hex header encoding option to ConsumeKafka (#11356)
    
    Add a 'Header Format' property (String/Hex) to ConsumeKafka so binary
    Kafka Record Header values can be written as a lowercase hexadecimal
    string instead of being corrupted by charset decoding. The existing
    'Header Encoding' property keeps its meaning (the character set used for
    the String format) and is shown only when Header Format is String, so
    existing flows are unchanged and no property migration is needed.
    
    Header decoding is resolved once into a HeaderValueConverter in
    onScheduled and applied uniformly to FlowFile attributes and the
    wrapper/inject-metadata record header fields. Internal batch headers
    (kafka.max.offset, kafka.count) decode as fixed UTF-8, matching how they
    are written.
    
    Also add unit tests for KafkaUtils.toKeyString and correct the KeyEncoding
    HEX description, which claimed uppercase output although HexFormat.of()
    emits lowercase.
---
 .../nifi/kafka/processors/ConsumeKafkaIT.java      | 32 ++++++++++++++++
 .../ConsumeKafkaInjectMetadataRecordIT.java        | 35 +++++++++++++++++
 .../processors/ConsumeKafkaWrapperRecordIT.java    | 34 +++++++++++++++++
 .../apache/nifi/kafka/processors/ConsumeKafka.java | 44 +++++++++++++++++-----
 .../processors/common/HeaderValueConverter.java    | 25 ++++++++++++
 .../nifi/kafka/processors/common/KafkaUtils.java   | 13 +++----
 .../consumer/bundle/ByteRecordBundler.java         | 10 ++---
 .../AbstractRecordStreamKafkaMessageConverter.java | 10 ++---
 .../FlowFileStreamKafkaMessageConverter.java       | 10 ++---
 ...ectOffsetRecordStreamKafkaMessageConverter.java |  6 +--
 .../convert/RecordStreamKafkaMessageConverter.java |  8 ++--
 .../WrapperRecordStreamKafkaMessageConverter.java  | 10 ++---
 .../consumer/wrapper/ConsumeWrapperRecord.java     | 10 ++---
 .../producer/wrapper/InjectMetadataRecord.java     |  5 ++-
 .../kafka/processors/common/KafkaUtilsTest.java    | 36 +++++++++++++++---
 .../RecordStreamKafkaMessageConverterTest.java     |  2 +-
 ...apperRecordStreamKafkaMessageConverterTest.java |  2 +-
 .../{KeyEncoding.java => HeaderFormat.java}        | 11 +++---
 .../nifi/kafka/shared/property/KeyEncoding.java    |  2 +-
 19 files changed, 241 insertions(+), 64 deletions(-)

diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaIT.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaIT.java
index e4e12898e38..76fe12d06ad 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaIT.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaIT.java
@@ -24,6 +24,7 @@ import 
org.apache.nifi.kafka.processors.consumer.ProcessingStrategy;
 import org.apache.nifi.kafka.service.Kafka3ConnectionService;
 import org.apache.nifi.kafka.service.api.consumer.AutoOffsetReset;
 import org.apache.nifi.kafka.shared.attribute.KafkaFlowFileAttribute;
+import org.apache.nifi.kafka.shared.property.HeaderFormat;
 import org.apache.nifi.reporting.InitializationException;
 import org.apache.nifi.util.MockFlowFile;
 import org.apache.nifi.util.TestRunner;
@@ -164,6 +165,37 @@ class ConsumeKafkaIT extends AbstractConsumeKafkaIT {
         flowFile.assertAttributeNotExists("custom"); // Non-core attributes 
should not exist without prefix
     }
 
+    @Test
+    void testProcessingStrategyFlowFileHexHeaderEncoding() throws 
InterruptedException, ExecutionException {
+        final String topic = UUID.randomUUID().toString();
+        final String groupId = topic.substring(0, topic.indexOf("-"));
+
+        runner.setProperty(ConsumeKafka.GROUP_ID, groupId);
+        runner.setProperty(ConsumeKafka.TOPICS, topic);
+        runner.setProperty(ConsumeKafka.PROCESSING_STRATEGY, 
ProcessingStrategy.FLOW_FILE.getValue());
+        runner.setProperty(ConsumeKafka.HEADER_NAME_PATTERN, ".*");
+        runner.setProperty(ConsumeKafka.HEADER_NAME_PREFIX, "kafka.header.");
+        runner.setProperty(ConsumeKafka.HEADER_FORMAT, 
HeaderFormat.HEX.getValue());
+
+        runner.run(1, false, true);
+
+        final byte[] binaryValue = new byte[] {0x01, 0x02, (byte) 0xab, (byte) 
0xff};
+        final List<Header> headers = List.of(new RecordHeader("binary", 
binaryValue));
+        produceOne(topic, 0, null, RECORD_VALUE, headers);
+        while (runner.getFlowFilesForRelationship("success").isEmpty()) {
+            runner.run(1, false, false);
+        }
+
+        runner.run(1, true, false);
+
+        final Iterator<MockFlowFile> flowFiles = 
runner.getFlowFilesForRelationship(ConsumeKafka.SUCCESS).iterator();
+        assertTrue(flowFiles.hasNext());
+
+        final MockFlowFile flowFile = flowFiles.next();
+        flowFile.assertContentEquals(RECORD_VALUE);
+        flowFile.assertAttributeEquals("kafka.header.binary", "0102abff");
+    }
+
     /**
      * Test ability to specify a topic regular expression to query for 
messages.
      */
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaInjectMetadataRecordIT.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaInjectMetadataRecordIT.java
index 84a4c8b2753..1c4524c44e8 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaInjectMetadataRecordIT.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaInjectMetadataRecordIT.java
@@ -27,6 +27,7 @@ import org.apache.kafka.common.header.internals.RecordHeader;
 import org.apache.nifi.kafka.processors.consumer.ProcessingStrategy;
 import org.apache.nifi.kafka.processors.producer.wrapper.InjectMetadataRecord;
 import org.apache.nifi.kafka.service.api.consumer.AutoOffsetReset;
+import org.apache.nifi.kafka.shared.property.HeaderFormat;
 import org.apache.nifi.kafka.shared.property.KeyFormat;
 import org.apache.nifi.kafka.shared.property.OutputStrategy;
 import org.apache.nifi.provenance.ProvenanceEventRecord;
@@ -36,6 +37,7 @@ import org.apache.nifi.util.MockFlowFile;
 import org.apache.nifi.util.TestRunner;
 import org.apache.nifi.util.TestRunners;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -186,4 +188,37 @@ class ConsumeKafkaInjectMetadataRecordIT extends 
AbstractConsumeKafkaIT {
         final ProvenanceEventRecord provenanceEvent = 
provenanceEvents.getFirst();
         assertEquals(ProvenanceEventType.RECEIVE, 
provenanceEvent.getEventType());
     }
+
+    @Test
+    void testInjectMetadataRecordHexHeaderEncoding()
+            throws InterruptedException, ExecutionException, IOException {
+        final String topic = UUID.randomUUID().toString();
+        final String groupId = topic.substring(0, topic.indexOf("-"));
+
+        runner.setProperty(ConsumeKafka.GROUP_ID, groupId);
+        runner.setProperty(ConsumeKafka.TOPICS, topic);
+        runner.setProperty(ConsumeKafka.PROCESSING_STRATEGY, 
ProcessingStrategy.RECORD);
+        runner.setProperty(ConsumeKafka.AUTO_OFFSET_RESET, 
AutoOffsetReset.EARLIEST);
+        runner.setProperty(ConsumeKafka.OUTPUT_STRATEGY, 
OutputStrategy.INJECT_METADATA);
+        runner.setProperty(ConsumeKafka.KEY_FORMAT, KeyFormat.STRING);
+        runner.setProperty(ConsumeKafka.HEADER_FORMAT, HeaderFormat.HEX);
+
+        runner.run(1, false, true);
+        final String message = new 
String(IOUtils.toByteArray(Objects.requireNonNull(
+                getClass().getClassLoader().getResource(TEST_RESOURCE))), 
StandardCharsets.UTF_8);
+        final byte[] binaryValue = new byte[] {0x01, 0x02, (byte) 0xab, (byte) 
0xff};
+        produceOne(topic, 0, MESSAGE_KEY, message, Collections.singletonList(
+                new RecordHeader("binary", binaryValue)));
+        while (runner.getFlowFilesForRelationship("success").isEmpty()) {
+            runner.run(1, false, false);
+        }
+        runner.run(1, true, false);
+
+        final MockFlowFile flowFile = 
runner.getFlowFilesForRelationship(ConsumeKafka.SUCCESS).getFirst();
+        final ArrayNode arrayNode = assertInstanceOf(ArrayNode.class, 
objectMapper.readTree(flowFile.getContent()));
+        final ObjectNode record = assertInstanceOf(ObjectNode.class, 
arrayNode.get(0));
+        final ObjectNode metadata = assertInstanceOf(ObjectNode.class, 
record.get(InjectMetadataRecord.METADATA));
+        final ObjectNode headers = assertInstanceOf(ObjectNode.class, 
metadata.get(InjectMetadataRecord.HEADERS));
+        assertEquals("0102abff", headers.get("binary").asText());
+    }
 }
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaWrapperRecordIT.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaWrapperRecordIT.java
index 7ce0469bbca..46ee56e3488 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaWrapperRecordIT.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-3-integration/src/test/java/org/apache/nifi/kafka/processors/ConsumeKafkaWrapperRecordIT.java
@@ -27,6 +27,7 @@ import org.apache.kafka.common.header.internals.RecordHeader;
 import org.apache.nifi.kafka.processors.consumer.ProcessingStrategy;
 import org.apache.nifi.kafka.processors.producer.wrapper.InjectMetadataRecord;
 import org.apache.nifi.kafka.service.api.consumer.AutoOffsetReset;
+import org.apache.nifi.kafka.shared.property.HeaderFormat;
 import org.apache.nifi.kafka.shared.property.KeyFormat;
 import org.apache.nifi.kafka.shared.property.OutputStrategy;
 import org.apache.nifi.provenance.ProvenanceEventRecord;
@@ -36,6 +37,7 @@ import org.apache.nifi.util.MockFlowFile;
 import org.apache.nifi.util.TestRunner;
 import org.apache.nifi.util.TestRunners;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -186,4 +188,36 @@ class ConsumeKafkaWrapperRecordIT extends 
AbstractConsumeKafkaIT {
         final ProvenanceEventRecord provenanceEvent = 
provenanceEvents.getFirst();
         assertEquals(ProvenanceEventType.RECEIVE, 
provenanceEvent.getEventType());
     }
+
+    @Test
+    void testWrapperRecordHexHeaderEncoding()
+            throws InterruptedException, ExecutionException, IOException {
+        final String topic = UUID.randomUUID().toString();
+        final String groupId = topic.substring(0, topic.indexOf("-"));
+
+        runner.setProperty(ConsumeKafka.GROUP_ID, groupId);
+        runner.setProperty(ConsumeKafka.TOPICS, topic);
+        runner.setProperty(ConsumeKafka.PROCESSING_STRATEGY, 
ProcessingStrategy.RECORD);
+        runner.setProperty(ConsumeKafka.AUTO_OFFSET_RESET, 
AutoOffsetReset.EARLIEST);
+        runner.setProperty(ConsumeKafka.OUTPUT_STRATEGY, 
OutputStrategy.USE_WRAPPER);
+        runner.setProperty(ConsumeKafka.KEY_FORMAT, KeyFormat.STRING);
+        runner.setProperty(ConsumeKafka.HEADER_FORMAT, HeaderFormat.HEX);
+
+        runner.run(1, false, true);
+        final String message = new 
String(IOUtils.toByteArray(Objects.requireNonNull(
+                getClass().getClassLoader().getResource(TEST_RESOURCE))), 
StandardCharsets.UTF_8);
+        final byte[] binaryValue = new byte[] {0x01, 0x02, (byte) 0xab, (byte) 
0xff};
+        produceOne(topic, 0, MESSAGE_KEY, message, Collections.singletonList(
+                new RecordHeader("binary", binaryValue)));
+        while (runner.getFlowFilesForRelationship("success").isEmpty()) {
+            runner.run(1, false, false);
+        }
+        runner.run(1, true, false);
+
+        final MockFlowFile flowFile = 
runner.getFlowFilesForRelationship(ConsumeKafka.SUCCESS).getFirst();
+        final ArrayNode arrayNode = assertInstanceOf(ArrayNode.class, 
objectMapper.readTree(flowFile.getContent()));
+        final ObjectNode wrapper = assertInstanceOf(ObjectNode.class, 
arrayNode.get(0));
+        final ObjectNode headers = assertInstanceOf(ObjectNode.class, 
wrapper.get("headers"));
+        assertEquals("0102abff", headers.get("binary").asText());
+    }
 }
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/ConsumeKafka.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/ConsumeKafka.java
index 9a50eb6060c..b489edbc662 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/ConsumeKafka.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/ConsumeKafka.java
@@ -32,6 +32,7 @@ import org.apache.nifi.components.PropertyValue;
 import org.apache.nifi.components.Validator;
 import org.apache.nifi.components.connector.components.ConnectorMethod;
 import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.kafka.processors.common.HeaderValueConverter;
 import org.apache.nifi.kafka.processors.common.KafkaUtils;
 import org.apache.nifi.kafka.processors.consumer.OffsetTracker;
 import org.apache.nifi.kafka.processors.consumer.ProcessingStrategy;
@@ -50,6 +51,7 @@ import 
org.apache.nifi.kafka.service.api.consumer.RebalanceCallback;
 import org.apache.nifi.kafka.service.api.consumer.SessionContext;
 import org.apache.nifi.kafka.service.api.record.ByteRecord;
 import org.apache.nifi.kafka.shared.attribute.KafkaFlowFileAttribute;
+import org.apache.nifi.kafka.shared.property.HeaderFormat;
 import org.apache.nifi.kafka.shared.property.KeyEncoding;
 import org.apache.nifi.kafka.shared.property.KeyFormat;
 import org.apache.nifi.kafka.shared.property.OutputStrategy;
@@ -75,6 +77,7 @@ import java.nio.charset.StandardCharsets;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HexFormat;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -94,7 +97,9 @@ import static 
org.apache.nifi.expression.ExpressionLanguageScope.NONE;
         + "configured Record Reader or Record Writer, the contents of the 
message will be written to a separate FlowFile, and that FlowFile will be 
transferred to the "
         + "'parse.failure' relationship. Otherwise, each FlowFile is sent to 
the 'success' relationship and may contain many individual messages within the 
single FlowFile. "
         + "A 'record.count' attribute is added to indicate how many messages 
are contained in the FlowFile. No two Kafka messages will be placed into the 
same FlowFile if they "
-        + "have different schemas, or if they have different values for a 
message header that is included by the <Headers to Add as Attributes> 
property.")
+        + "have different schemas, or if they have different values for a 
message header that is included by the <Headers to Add as Attributes> property. 
"
+        + "Kafka Record Header values selected for output are represented 
according to the Header Format property: as text decoded with the configured 
Header Encoding "
+        + "character set, or as a lowercase hexadecimal string for binary-safe 
output.")
 @Tags({"Kafka", "Get", "Record", "csv", "avro", "json", "Ingest", "Ingress", 
"Topic", "PubSub", "Consume"})
 @WritesAttributes({
         @WritesAttribute(attribute = "record.count", description = "The number 
of records received"),
@@ -194,12 +199,21 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
             .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
             .build();
 
+    static final PropertyDescriptor HEADER_FORMAT = new 
PropertyDescriptor.Builder()
+            .name("Header Format")
+            .description("Specifies how Kafka Record Header values are 
represented when written as FlowFile attributes or record fields")
+            .required(true)
+            .defaultValue(HeaderFormat.STRING)
+            .allowableValues(HeaderFormat.class)
+            .build();
+
     static final PropertyDescriptor HEADER_ENCODING = new 
PropertyDescriptor.Builder()
             .name("Header Encoding")
             .description("Character encoding applied when reading Kafka Record 
Header values and writing FlowFile attributes")
             .addValidator(StandardValidators.CHARACTER_SET_VALIDATOR)
             .defaultValue(StandardCharsets.UTF_8.name())
             .required(true)
+            .dependsOn(HEADER_FORMAT, HeaderFormat.STRING)
             .build();
 
     static final PropertyDescriptor HEADER_NAME_PATTERN = new 
PropertyDescriptor.Builder()
@@ -322,6 +336,7 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
             MAX_UNCOMMITTED_SIZE,
             MAX_UNCOMMITTED_TIME,
             HEADER_NAME_PATTERN,
+            HEADER_FORMAT,
             HEADER_ENCODING,
             PROCESSING_STRATEGY,
             HEADER_NAME_PREFIX,
@@ -338,7 +353,7 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
     private static final Set<Relationship> SUCCESS_RELATIONSHIP = 
Set.of(SUCCESS);
     private static final Set<Relationship> SUCCESS_FAILURE_RELATIONSHIPS = 
Set.of(SUCCESS, PARSE_FAILURE);
 
-    private volatile Charset headerEncoding;
+    private volatile HeaderValueConverter headerValueConverter;
     private volatile Pattern headerNamePattern;
     private volatile String headerNamePrefix;
     private volatile ProcessingStrategy processingStrategy;
@@ -373,10 +388,21 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
         }
     }
 
+    private static HeaderValueConverter createHeaderValueConverter(final 
ProcessContext context) {
+        final HeaderFormat headerFormat = 
context.getProperty(HEADER_FORMAT).asAllowableValue(HeaderFormat.class);
+        return switch (headerFormat) {
+            case STRING -> {
+                final Charset charset = 
Charset.forName(context.getProperty(HEADER_ENCODING).getValue());
+                yield value -> new String(value, charset);
+            }
+            case HEX -> value -> HexFormat.of().formatHex(value);
+        };
+    }
+
     @OnScheduled
     public void onScheduled(final ProcessContext context) {
         pollingContext = createPollingContext(context);
-        headerEncoding = 
Charset.forName(context.getProperty(HEADER_ENCODING).getValue());
+        headerValueConverter = createHeaderValueConverter(context);
 
         final String headerNamePatternProperty = 
context.getProperty(HEADER_NAME_PATTERN).getValue();
         if (StringUtils.isNotBlank(headerNamePatternProperty)) {
@@ -634,7 +660,7 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
         for (final ByteRecord byteRecord : records) {
             recordIndex++;
             final Map<String, String> recordAttributes = 
KafkaUtils.toAttributes(
-                byteRecord, keyEncoding, headerNamePattern, headerEncoding, 
commitOffsets);
+                byteRecord, keyEncoding, headerNamePattern, 
headerValueConverter, commitOffsets);
 
             try (final InputStream inputStream = new 
ByteArrayInputStream(byteRecord.getValue());
                  final RecordReader reader = 
readerFactory.createRecordReader(recordAttributes, inputStream, 
byteRecord.getValue().length, verificationLogger)) {
@@ -765,7 +791,7 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
 
         final byte[] demarcator = 
demarcatorValue.getBytes(StandardCharsets.UTF_8);
         final boolean separateByKey = 
context.getProperty(SEPARATE_BY_KEY).asBoolean();
-        return new ByteRecordBundler(demarcator, separateByKey, keyEncoding, 
headerNamePattern, headerEncoding, commitOffsets).bundle(consumerRecords);
+        return new ByteRecordBundler(demarcator, separateByKey, keyEncoding, 
headerNamePattern, headerValueConverter, commitOffsets).bundle(consumerRecords);
     }
 
     private void processInputRecords(final ProcessContext context, final 
ProcessSession session, final OffsetTracker offsetTracker, final 
Iterator<ByteRecord> consumerRecords) {
@@ -774,13 +800,13 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
 
         final KafkaMessageConverter converter;
         if (outputStrategy == OutputStrategy.USE_VALUE) {
-            converter = new RecordStreamKafkaMessageConverter(readerFactory, 
writerFactory, headerEncoding, headerNamePattern,
+            converter = new RecordStreamKafkaMessageConverter(readerFactory, 
writerFactory, headerValueConverter, headerNamePattern,
                     keyEncoding, commitOffsets, offsetTracker, getLogger(), 
brokerUri);
         } else if (outputStrategy == OutputStrategy.INJECT_OFFSET) {
             converter = new InjectOffsetRecordStreamKafkaMessageConverter(
                     readerFactory,
                     writerFactory,
-                    headerEncoding,
+                    headerValueConverter,
                     headerNamePattern,
                     keyEncoding,
                     commitOffsets,
@@ -793,7 +819,7 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
                 ? 
context.getProperty(KEY_RECORD_READER).asControllerService(RecordReaderFactory.class)
 : null;
 
             converter = new 
WrapperRecordStreamKafkaMessageConverter(readerFactory, writerFactory, 
keyReaderFactory,
-                headerEncoding, headerNamePattern, keyFormat, keyEncoding, 
commitOffsets, offsetTracker, getLogger(), brokerUri, outputStrategy);
+                headerValueConverter, headerNamePattern, keyFormat, 
keyEncoding, commitOffsets, offsetTracker, getLogger(), brokerUri, 
outputStrategy);
         }
 
         converter.toFlowFiles(session, consumerRecords);
@@ -801,7 +827,7 @@ public class ConsumeKafka extends AbstractProcessor 
implements VerifiableProcess
 
     private void processInputFlowFile(final ProcessSession session, final 
OffsetTracker offsetTracker, final Iterator<ByteRecord> consumerRecords) {
         final KafkaMessageConverter converter = new 
FlowFileStreamKafkaMessageConverter(
-            headerEncoding, headerNamePattern, headerNamePrefix, keyEncoding, 
commitOffsets, offsetTracker, brokerUri);
+            headerValueConverter, headerNamePattern, headerNamePrefix, 
keyEncoding, commitOffsets, offsetTracker, brokerUri);
         converter.toFlowFiles(session, consumerRecords);
     }
 
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/common/HeaderValueConverter.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/common/HeaderValueConverter.java
new file mode 100644
index 00000000000..eeca62a4aa2
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/common/HeaderValueConverter.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.kafka.processors.common;
+
+/**
+ * Converts a Kafka Record Header value to the String form written as a 
FlowFile attribute or record field.
+ */
+@FunctionalInterface
+public interface HeaderValueConverter {
+    String convert(byte[] value);
+}
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/common/KafkaUtils.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/common/KafkaUtils.java
index 40e1e373678..853640f4110 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/common/KafkaUtils.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/common/KafkaUtils.java
@@ -21,7 +21,6 @@ import org.apache.nifi.kafka.service.api.record.ByteRecord;
 import org.apache.nifi.kafka.shared.attribute.KafkaFlowFileAttribute;
 import org.apache.nifi.kafka.shared.property.KeyEncoding;
 
-import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -72,14 +71,14 @@ public class KafkaUtils {
     }
 
     public static Map<String, String> toAttributes(final ByteRecord 
consumerRecord, final KeyEncoding keyEncoding,
-                                                   final Pattern 
headerNamePattern, final Charset headerEncoding,
+                                                   final Pattern 
headerNamePattern, final HeaderValueConverter headerValueConverter,
                                                    final boolean 
commitOffsets) {
-        return toAttributes(consumerRecord, keyEncoding, headerNamePattern, 
null, headerEncoding, commitOffsets);
+        return toAttributes(consumerRecord, keyEncoding, headerNamePattern, 
null, headerValueConverter, commitOffsets);
     }
 
     public static Map<String, String> toAttributes(final ByteRecord 
consumerRecord, final KeyEncoding keyEncoding,
                                                    final Pattern 
headerNamePattern, final String headerNamePrefix,
-                                                   final Charset 
headerEncoding, final boolean commitOffsets) {
+                                                   final HeaderValueConverter 
headerValueConverter, final boolean commitOffsets) {
         final Map<String, String> attributes = new HashMap<>();
         attributes.put(KafkaFlowFileAttribute.KAFKA_TOPIC, 
consumerRecord.getTopic());
         attributes.put(KafkaFlowFileAttribute.KAFKA_PARTITION, 
Long.toString(consumerRecord.getPartition()));
@@ -87,10 +86,10 @@ public class KafkaUtils {
         
attributes.put(KafkaFlowFileAttribute.KAFKA_CONSUMER_OFFSETS_COMMITTED, 
String.valueOf(commitOffsets));
         consumerRecord.getHeaders().stream()
                 .filter(h -> 
h.key().equals(KafkaFlowFileAttribute.KAFKA_MAX_OFFSET)).findFirst()
-                .ifPresent(h -> 
attributes.put(KafkaFlowFileAttribute.KAFKA_MAX_OFFSET, new String(h.value(), 
headerEncoding)));
+                .ifPresent(h -> 
attributes.put(KafkaFlowFileAttribute.KAFKA_MAX_OFFSET, new String(h.value(), 
StandardCharsets.UTF_8)));
         consumerRecord.getHeaders().stream()
                 .filter(h -> 
h.key().equals(KafkaFlowFileAttribute.KAFKA_COUNT)).findFirst()
-                .ifPresent(h -> 
attributes.put(KafkaFlowFileAttribute.KAFKA_COUNT, new String(h.value(), 
headerEncoding)));
+                .ifPresent(h -> 
attributes.put(KafkaFlowFileAttribute.KAFKA_COUNT, new String(h.value(), 
StandardCharsets.UTF_8)));
         attributes.put(KafkaFlowFileAttribute.KAFKA_TIMESTAMP, 
Long.toString(consumerRecord.getTimestamp()));
         Optional.ofNullable(toKeyString(consumerRecord.getKey().orElse(null), 
keyEncoding))
                 .ifPresent(keyAttribute -> 
attributes.put(KafkaFlowFileAttribute.KAFKA_KEY, keyAttribute));
@@ -102,7 +101,7 @@ public class KafkaUtils {
             for (final RecordHeader header : headers) {
                 final String name = header.key();
                 if (headerNamePattern.matcher(name).matches()) {
-                    final String value = new String(header.value(), 
headerEncoding);
+                    final String value = 
headerValueConverter.convert(header.value());
                     final String attributeName = headerNamePrefix != null ? 
headerNamePrefix + name : name;
                     attributes.put(attributeName, value);
                 }
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/bundle/ByteRecordBundler.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/bundle/ByteRecordBundler.java
index a0144aa48ef..c47e308cfe6 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/bundle/ByteRecordBundler.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/bundle/ByteRecordBundler.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.kafka.processors.consumer.bundle;
 
+import org.apache.nifi.kafka.processors.common.HeaderValueConverter;
 import org.apache.nifi.kafka.processors.common.KafkaUtils;
 import org.apache.nifi.kafka.service.api.common.TopicPartitionSummary;
 import org.apache.nifi.kafka.service.api.header.RecordHeader;
@@ -23,7 +24,6 @@ import org.apache.nifi.kafka.service.api.record.ByteRecord;
 import org.apache.nifi.kafka.shared.attribute.KafkaFlowFileAttribute;
 import org.apache.nifi.kafka.shared.property.KeyEncoding;
 
-import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -36,18 +36,18 @@ public class ByteRecordBundler {
     private final boolean separateByKey;
     private final KeyEncoding keyEncoding;
     private final Pattern headerNamePattern;
-    private final Charset headerEncoding;
+    private final HeaderValueConverter headerValueConverter;
     private final boolean commitOffsets;
 
     private final Map<BundleKey, BundleValue> bundles;
 
     public ByteRecordBundler(final byte[] demarcator, final boolean 
separateByKey, final KeyEncoding keyEncoding,
-                             final Pattern headerNamePattern, final Charset 
headerEncoding, final boolean commitOffsets) {
+                             final Pattern headerNamePattern, final 
HeaderValueConverter headerValueConverter, final boolean commitOffsets) {
         this.demarcator = demarcator;
         this.separateByKey = separateByKey;
         this.keyEncoding = keyEncoding;
         this.headerNamePattern = headerNamePattern;
-        this.headerEncoding = headerEncoding;
+        this.headerValueConverter = headerValueConverter;
         this.commitOffsets = commitOffsets;
         this.bundles = new HashMap<>();
     }
@@ -74,7 +74,7 @@ public class ByteRecordBundler {
         final List<RecordHeader> headers = byteRecord.getHeaders();
         final List<RecordHeader> headersFiltered = 
KafkaUtils.toHeadersFiltered(byteRecord, headerNamePattern);
         final byte[] messageKey = (separateByKey ? 
byteRecord.getKey().orElse(null) : null);
-        final Map<String, String> attributes = 
KafkaUtils.toAttributes(byteRecord, keyEncoding, headerNamePattern, 
headerEncoding, commitOffsets);
+        final Map<String, String> attributes = 
KafkaUtils.toAttributes(byteRecord, keyEncoding, headerNamePattern, 
headerValueConverter, commitOffsets);
         final BundleKey bundleKey = new BundleKey(topicPartition, 
byteRecord.getTimestamp(), headers, headersFiltered, attributes, messageKey);
         if (bundles.containsKey(bundleKey)) {
             update(bundles, byteRecord, bundleKey);
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/AbstractRecordStreamKafkaMessageConverter.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/AbstractRecordStreamKafkaMessageConverter.java
index 5163bdf7353..c1f8baff634 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/AbstractRecordStreamKafkaMessageConverter.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/AbstractRecordStreamKafkaMessageConverter.java
@@ -19,6 +19,7 @@ package org.apache.nifi.kafka.processors.consumer.convert;
 import org.apache.nifi.flowfile.FlowFile;
 import org.apache.nifi.flowfile.attributes.CoreAttributes;
 import org.apache.nifi.kafka.processors.ConsumeKafka;
+import org.apache.nifi.kafka.processors.common.HeaderValueConverter;
 import org.apache.nifi.kafka.processors.common.KafkaUtils;
 import org.apache.nifi.kafka.processors.consumer.OffsetTracker;
 import org.apache.nifi.kafka.service.api.record.ByteRecord;
@@ -42,7 +43,6 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.nio.charset.Charset;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -55,7 +55,7 @@ public abstract class 
AbstractRecordStreamKafkaMessageConverter implements Kafka
 
     protected final RecordReaderFactory readerFactory;
     protected final RecordSetWriterFactory writerFactory;
-    protected final Charset headerEncoding;
+    protected final HeaderValueConverter headerValueConverter;
     protected final Pattern headerNamePattern;
     protected final KeyEncoding keyEncoding;
     protected final boolean commitOffsets;
@@ -66,7 +66,7 @@ public abstract class 
AbstractRecordStreamKafkaMessageConverter implements Kafka
     public AbstractRecordStreamKafkaMessageConverter(
             final RecordReaderFactory readerFactory,
             final RecordSetWriterFactory writerFactory,
-            final Charset headerEncoding,
+            final HeaderValueConverter headerValueConverter,
             final Pattern headerNamePattern,
             final KeyEncoding keyEncoding,
             final boolean commitOffsets,
@@ -75,7 +75,7 @@ public abstract class 
AbstractRecordStreamKafkaMessageConverter implements Kafka
             final String brokerUri) {
         this.readerFactory = readerFactory;
         this.writerFactory = writerFactory;
-        this.headerEncoding = headerEncoding;
+        this.headerValueConverter = headerValueConverter;
         this.headerNamePattern = headerNamePattern;
         this.keyEncoding = keyEncoding;
         this.commitOffsets = commitOffsets;
@@ -96,7 +96,7 @@ public abstract class 
AbstractRecordStreamKafkaMessageConverter implements Kafka
 
             // shared attribute extraction
             final Map<String, String> attributes = KafkaUtils.toAttributes(
-                    consumerRecord, keyEncoding, headerNamePattern, 
headerEncoding, commitOffsets);
+                    consumerRecord, keyEncoding, headerNamePattern, 
headerValueConverter, commitOffsets);
 
             // hook for subclasses to expose headers (if desired)
             final Map<String, String> extraAttrs = 
extractHeaders(consumerRecord);
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/FlowFileStreamKafkaMessageConverter.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/FlowFileStreamKafkaMessageConverter.java
index 815ddcb32be..f0289950bb0 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/FlowFileStreamKafkaMessageConverter.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/FlowFileStreamKafkaMessageConverter.java
@@ -18,6 +18,7 @@ package org.apache.nifi.kafka.processors.consumer.convert;
 
 import org.apache.nifi.flowfile.FlowFile;
 import org.apache.nifi.kafka.processors.ConsumeKafka;
+import org.apache.nifi.kafka.processors.common.HeaderValueConverter;
 import org.apache.nifi.kafka.processors.common.KafkaUtils;
 import org.apache.nifi.kafka.processors.consumer.OffsetTracker;
 import org.apache.nifi.kafka.service.api.record.ByteRecord;
@@ -25,13 +26,12 @@ import 
org.apache.nifi.kafka.shared.attribute.KafkaFlowFileAttribute;
 import org.apache.nifi.kafka.shared.property.KeyEncoding;
 import org.apache.nifi.processor.ProcessSession;
 
-import java.nio.charset.Charset;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.regex.Pattern;
 
 public class FlowFileStreamKafkaMessageConverter implements 
KafkaMessageConverter {
-    private final Charset headerEncoding;
+    private final HeaderValueConverter headerValueConverter;
     private final Pattern headerNamePattern;
     private final String headerNamePrefix;
     private final KeyEncoding keyEncoding;
@@ -40,14 +40,14 @@ public class FlowFileStreamKafkaMessageConverter implements 
KafkaMessageConverte
     private final String brokerUri;
 
     public FlowFileStreamKafkaMessageConverter(
-            final Charset headerEncoding,
+            final HeaderValueConverter headerValueConverter,
             final Pattern headerNamePattern,
             final String headerNamePrefix,
             final KeyEncoding keyEncoding,
             final boolean commitOffsets,
             final OffsetTracker offsetTracker,
             final String brokerUri) {
-        this.headerEncoding = headerEncoding;
+        this.headerValueConverter = headerValueConverter;
         this.headerNamePattern = headerNamePattern;
         this.headerNamePrefix = headerNamePrefix;
         this.keyEncoding = keyEncoding;
@@ -71,7 +71,7 @@ public class FlowFileStreamKafkaMessageConverter implements 
KafkaMessageConverte
             }
 
             final Map<String, String> attributes = KafkaUtils.toAttributes(
-                    consumerRecord, keyEncoding, headerNamePattern, 
headerNamePrefix, headerEncoding, commitOffsets);
+                    consumerRecord, keyEncoding, headerNamePattern, 
headerNamePrefix, headerValueConverter, commitOffsets);
             flowFile = session.putAllAttributes(flowFile, attributes);
 
             session.getProvenanceReporter().receive(flowFile, brokerUri + "/" 
+ consumerRecord.getTopic());
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/InjectOffsetRecordStreamKafkaMessageConverter.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/InjectOffsetRecordStreamKafkaMessageConverter.java
index 49b4f86b82a..a71d9c791c6 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/InjectOffsetRecordStreamKafkaMessageConverter.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/InjectOffsetRecordStreamKafkaMessageConverter.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.kafka.processors.consumer.convert;
 
+import org.apache.nifi.kafka.processors.common.HeaderValueConverter;
 import org.apache.nifi.kafka.processors.consumer.OffsetTracker;
 import org.apache.nifi.kafka.service.api.record.ByteRecord;
 import org.apache.nifi.kafka.shared.property.KeyEncoding;
@@ -29,7 +30,6 @@ import org.apache.nifi.serialization.record.RecordField;
 import org.apache.nifi.serialization.record.RecordFieldType;
 import org.apache.nifi.serialization.record.RecordSchema;
 
-import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -48,7 +48,7 @@ public class InjectOffsetRecordStreamKafkaMessageConverter 
extends AbstractRecor
     public InjectOffsetRecordStreamKafkaMessageConverter(
             final RecordReaderFactory readerFactory,
             final RecordSetWriterFactory writerFactory,
-            final Charset headerEncoding,
+            final HeaderValueConverter headerValueConverter,
             final Pattern headerNamePattern,
             final KeyEncoding keyEncoding,
             final boolean commitOffsets,
@@ -59,7 +59,7 @@ public class InjectOffsetRecordStreamKafkaMessageConverter 
extends AbstractRecor
         super(
                 readerFactory,
                 writerFactory,
-                headerEncoding,
+                headerValueConverter,
                 headerNamePattern,
                 keyEncoding,
                 commitOffsets,
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/RecordStreamKafkaMessageConverter.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/RecordStreamKafkaMessageConverter.java
index 4bf1fe882c6..5309db99dc9 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/RecordStreamKafkaMessageConverter.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/RecordStreamKafkaMessageConverter.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.kafka.processors.consumer.convert;
 
+import org.apache.nifi.kafka.processors.common.HeaderValueConverter;
 import org.apache.nifi.kafka.processors.consumer.OffsetTracker;
 import org.apache.nifi.kafka.service.api.header.RecordHeader;
 import org.apache.nifi.kafka.service.api.record.ByteRecord;
@@ -28,7 +29,6 @@ import org.apache.nifi.serialization.record.Record;
 import org.apache.nifi.serialization.record.RecordSchema;
 
 import java.io.IOException;
-import java.nio.charset.Charset;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.regex.Pattern;
@@ -38,14 +38,14 @@ public class RecordStreamKafkaMessageConverter extends 
AbstractRecordStreamKafka
     public RecordStreamKafkaMessageConverter(
             final RecordReaderFactory readerFactory,
             final RecordSetWriterFactory writerFactory,
-            final Charset headerEncoding,
+            final HeaderValueConverter headerValueConverter,
             final Pattern headerNamePattern,
             final KeyEncoding keyEncoding,
             final boolean commitOffsets,
             final OffsetTracker offsetTracker,
             final ComponentLog logger,
             final String brokerUri) {
-        super(readerFactory, writerFactory, headerEncoding, headerNamePattern, 
keyEncoding, commitOffsets, offsetTracker, logger, brokerUri);
+        super(readerFactory, writerFactory, headerValueConverter, 
headerNamePattern, keyEncoding, commitOffsets, offsetTracker, logger, 
brokerUri);
     }
 
     @Override
@@ -72,7 +72,7 @@ public class RecordStreamKafkaMessageConverter extends 
AbstractRecordStreamKafka
         for (final RecordHeader h : consumerRecord.getHeaders()) {
             final String name = h.key();
             if (headerNamePattern.matcher(name).matches()) {
-                headers.put(name, new String(h.value(), headerEncoding));
+                headers.put(name, headerValueConverter.convert(h.value()));
             }
         }
         return headers;
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/WrapperRecordStreamKafkaMessageConverter.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/WrapperRecordStreamKafkaMessageConverter.java
index 0ed9fc84938..a4a3b4dd99a 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/WrapperRecordStreamKafkaMessageConverter.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/convert/WrapperRecordStreamKafkaMessageConverter.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.kafka.processors.consumer.convert;
 
+import org.apache.nifi.kafka.processors.common.HeaderValueConverter;
 import org.apache.nifi.kafka.processors.consumer.OffsetTracker;
 import org.apache.nifi.kafka.processors.consumer.wrapper.ConsumeWrapperRecord;
 import 
org.apache.nifi.kafka.processors.consumer.wrapper.WrapperRecordKeyReader;
@@ -36,7 +37,6 @@ import org.apache.nifi.serialization.record.RecordSchema;
 import org.apache.nifi.util.Tuple;
 
 import java.io.IOException;
-import java.nio.charset.Charset;
 import java.util.Map;
 
 public class WrapperRecordStreamKafkaMessageConverter extends 
AbstractRecordStreamKafkaMessageConverter {
@@ -49,7 +49,7 @@ public class WrapperRecordStreamKafkaMessageConverter extends 
AbstractRecordStre
             final RecordReaderFactory readerFactory,
             final RecordSetWriterFactory writerFactory,
             final RecordReaderFactory keyReaderFactory,
-            final Charset headerEncoding,
+            final HeaderValueConverter headerValueConverter,
             final java.util.regex.Pattern headerNamePattern,
             final KeyFormat keyFormat,
             final KeyEncoding keyEncoding,
@@ -58,7 +58,7 @@ public class WrapperRecordStreamKafkaMessageConverter extends 
AbstractRecordStre
             final ComponentLog logger,
             final String brokerUri,
             final OutputStrategy outputStrategy) {
-        super(readerFactory, writerFactory, headerEncoding, headerNamePattern, 
keyEncoding, commitOffsets, offsetTracker, logger, brokerUri);
+        super(readerFactory, writerFactory, headerValueConverter, 
headerNamePattern, keyEncoding, commitOffsets, offsetTracker, logger, 
brokerUri);
         this.keyReaderFactory = keyReaderFactory;
         this.keyFormat = keyFormat;
         this.outputStrategy = outputStrategy;
@@ -84,8 +84,8 @@ public class WrapperRecordStreamKafkaMessageConverter extends 
AbstractRecordStre
             final WrapperRecordKeyReader keyReader = new 
WrapperRecordKeyReader(keyFormat, keyReaderFactory, keyEncoding, logger);
             final Tuple<RecordField, Object> recordKey = 
keyReader.toWrapperRecordKey(consumerRecord.getKey().orElse(null), attributes);
             return outputStrategy == OutputStrategy.USE_WRAPPER
-                    ? new 
ConsumeWrapperRecord(headerEncoding).toWrapperRecord(consumerRecord, record, 
recordKey)
-                    : InjectMetadataRecord.toWrapperRecord(headerEncoding, 
consumerRecord, record, recordKey);
+                    ? new 
ConsumeWrapperRecord(headerValueConverter).toWrapperRecord(consumerRecord, 
record, recordKey)
+                    : 
InjectMetadataRecord.toWrapperRecord(headerValueConverter, consumerRecord, 
record, recordKey);
         } catch (IOException | SchemaNotFoundException | 
MalformedRecordException e) {
             throw new IOException("Unable to convert record", e);
         }
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/wrapper/ConsumeWrapperRecord.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/wrapper/ConsumeWrapperRecord.java
index edbc086bf33..0018859e57a 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/wrapper/ConsumeWrapperRecord.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/consumer/wrapper/ConsumeWrapperRecord.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.kafka.processors.consumer.wrapper;
 
+import org.apache.nifi.kafka.processors.common.HeaderValueConverter;
 import org.apache.nifi.kafka.processors.producer.wrapper.WrapperRecord;
 import org.apache.nifi.kafka.service.api.header.RecordHeader;
 import org.apache.nifi.kafka.service.api.record.ByteRecord;
@@ -27,7 +28,6 @@ import org.apache.nifi.serialization.record.RecordFieldType;
 import org.apache.nifi.serialization.record.RecordSchema;
 import org.apache.nifi.util.Tuple;
 
-import java.nio.charset.Charset;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -36,10 +36,10 @@ public class ConsumeWrapperRecord {
 
     private static final RecordSchema EMPTY_SCHEMA = new 
SimpleRecordSchema(List.of());
 
-    private final Charset headerCharacterSet;
+    private final HeaderValueConverter headerValueConverter;
 
-    public ConsumeWrapperRecord(final Charset headerCharacterSet) {
-        this.headerCharacterSet = headerCharacterSet;
+    public ConsumeWrapperRecord(final HeaderValueConverter 
headerValueConverter) {
+        this.headerValueConverter = headerValueConverter;
     }
 
     public MapRecord toWrapperRecord(final ByteRecord consumerRecord,
@@ -72,7 +72,7 @@ public class ConsumeWrapperRecord {
         final RecordField recordField = new RecordField(WrapperRecord.HEADERS, 
RecordFieldType.MAP.getMapDataType(RecordFieldType.STRING.getDataType()));
         final Map<String, String> headers = new HashMap<>();
         for (final RecordHeader header : consumerRecord.getHeaders()) {
-            headers.put(header.key(), new String(header.value(), 
headerCharacterSet));
+            headers.put(header.key(), 
headerValueConverter.convert(header.value()));
         }
         return new Tuple<>(recordField, headers);
     }
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/producer/wrapper/InjectMetadataRecord.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/producer/wrapper/InjectMetadataRecord.java
index b13c16348e2..a637cd60b8e 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/producer/wrapper/InjectMetadataRecord.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/main/java/org/apache/nifi/kafka/processors/producer/wrapper/InjectMetadataRecord.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.kafka.processors.producer.wrapper;
 
+import org.apache.nifi.kafka.processors.common.HeaderValueConverter;
 import org.apache.nifi.kafka.service.api.header.RecordHeader;
 import org.apache.nifi.kafka.service.api.record.ByteRecord;
 import org.apache.nifi.serialization.SimpleRecordSchema;
@@ -108,7 +109,7 @@ public class InjectMetadataRecord extends MapRecord {
         return new SimpleRecordSchema(valueFieldsWithInjectedMetadata);
     }
 
-    public static MapRecord toWrapperRecord(final Charset headerCharacterSet, 
final ByteRecord consumerRecord, final Record record, final Tuple<RecordField, 
Object> tupleKey) {
+    public static MapRecord toWrapperRecord(final HeaderValueConverter 
headerValueConverter, final ByteRecord consumerRecord, final Record record, 
final Tuple<RecordField, Object> tupleKey) {
         final RecordSchema schema = record.getSchema();
         RecordSchema finalSchema = 
InjectMetadataRecord.toWrapperSchema(tupleKey.getKey(), schema);
 
@@ -123,7 +124,7 @@ public class InjectMetadataRecord extends MapRecord {
 
         final Map<String, Object> valuesHeaders = new HashMap<>();
         for (RecordHeader header : consumerRecord.getHeaders()) {
-            valuesHeaders.put(header.key(), new String(header.value(), 
headerCharacterSet));
+            valuesHeaders.put(header.key(), 
headerValueConverter.convert(header.value()));
         }
         valuesMetadata.put(InjectMetadataRecord.HEADERS, valuesHeaders);
 
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/common/KafkaUtilsTest.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/common/KafkaUtilsTest.java
index 7aeedeb3823..e319fd44a4f 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/common/KafkaUtilsTest.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/common/KafkaUtilsTest.java
@@ -29,6 +29,7 @@ import java.util.regex.Pattern;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
 
 class KafkaUtilsTest {
 
@@ -37,6 +38,8 @@ class KafkaUtilsTest {
     private static final long TEST_OFFSET = 100L;
     private static final long TEST_TIMESTAMP = System.currentTimeMillis();
 
+    private static final HeaderValueConverter STRING_CONVERTER = value -> new 
String(value, StandardCharsets.UTF_8);
+
     @Test
     void testToAttributesWithoutPrefix() {
         final List<RecordHeader> headers = List.of(
@@ -48,7 +51,7 @@ class KafkaUtilsTest {
         final Pattern headerPattern = Pattern.compile(".*");
 
         final Map<String, String> attributes = KafkaUtils.toAttributes(
-                record, KeyEncoding.UTF8, headerPattern, 
StandardCharsets.UTF_8, true);
+                record, KeyEncoding.UTF8, headerPattern, STRING_CONVERTER, 
true);
 
         // Verify standard Kafka attributes
         assertEquals(TEST_TOPIC, 
attributes.get(KafkaFlowFileAttribute.KAFKA_TOPIC));
@@ -73,7 +76,7 @@ class KafkaUtilsTest {
         final String prefix = "kafka.header.";
 
         final Map<String, String> attributes = KafkaUtils.toAttributes(
-                record, KeyEncoding.UTF8, headerPattern, prefix, 
StandardCharsets.UTF_8, true);
+                record, KeyEncoding.UTF8, headerPattern, prefix, 
STRING_CONVERTER, true);
 
         // Verify standard Kafka attributes are NOT prefixed
         assertEquals(TEST_TOPIC, 
attributes.get(KafkaFlowFileAttribute.KAFKA_TOPIC));
@@ -105,7 +108,7 @@ class KafkaUtilsTest {
         final String prefix = "msg.";
 
         final Map<String, String> attributes = KafkaUtils.toAttributes(
-                record, KeyEncoding.UTF8, headerPattern, prefix, 
StandardCharsets.UTF_8, true);
+                record, KeyEncoding.UTF8, headerPattern, prefix, 
STRING_CONVERTER, true);
 
         // Verify matching headers are added with prefix
         assertEquals("test-uuid", attributes.get("msg.uuid"));
@@ -130,7 +133,7 @@ class KafkaUtilsTest {
 
         // Explicitly pass null for prefix
         final Map<String, String> attributes = KafkaUtils.toAttributes(
-                record, KeyEncoding.UTF8, headerPattern, null, 
StandardCharsets.UTF_8, true);
+                record, KeyEncoding.UTF8, headerPattern, null, 
STRING_CONVERTER, true);
 
         // Verify header is added without prefix when prefix is null
         assertEquals("value1", attributes.get("header1"));
@@ -148,12 +151,35 @@ class KafkaUtilsTest {
 
         // Pass empty string for prefix
         final Map<String, String> attributes = KafkaUtils.toAttributes(
-                record, KeyEncoding.UTF8, headerPattern, "", 
StandardCharsets.UTF_8, true);
+                record, KeyEncoding.UTF8, headerPattern, "", STRING_CONVERTER, 
true);
 
         // Verify header is added with empty prefix (effectively no prefix)
         assertEquals("value1", attributes.get("header1"));
     }
 
+    @Test
+    void testToKeyStringUtf8() {
+        final byte[] key = "my-key".getBytes(StandardCharsets.UTF_8);
+        assertEquals("my-key", KafkaUtils.toKeyString(key, KeyEncoding.UTF8));
+    }
+
+    @Test
+    void testToKeyStringHexIsLowercase() {
+        final byte[] key = new byte[] {0x01, 0x02, (byte) 0xab, (byte) 0xff};
+        assertEquals("0102abff", KafkaUtils.toKeyString(key, KeyEncoding.HEX));
+    }
+
+    @Test
+    void testToKeyStringDoNotAdd() {
+        final byte[] key = "my-key".getBytes(StandardCharsets.UTF_8);
+        assertNull(KafkaUtils.toKeyString(key, KeyEncoding.DO_NOT_ADD));
+    }
+
+    @Test
+    void testToKeyStringNullKey() {
+        assertNull(KafkaUtils.toKeyString(null, KeyEncoding.UTF8));
+    }
+
     private ByteRecord createByteRecord(final List<RecordHeader> headers) {
         return new ByteRecord(
                 TEST_TOPIC,
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/convert/RecordStreamKafkaMessageConverterTest.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/convert/RecordStreamKafkaMessageConverterTest.java
index e85ce4a85fd..2f694ed310a 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/convert/RecordStreamKafkaMessageConverterTest.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/convert/RecordStreamKafkaMessageConverterTest.java
@@ -72,7 +72,7 @@ class RecordStreamKafkaMessageConverterTest {
         final RecordStreamKafkaMessageConverter converter = new 
RecordStreamKafkaMessageConverter(
                 readerFactory,
                 writerFactory,
-                StandardCharsets.UTF_8,
+                value -> new String(value, StandardCharsets.UTF_8),
                 Pattern.compile(".*"),
                 KeyEncoding.UTF8,
                 true,
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/convert/WrapperRecordStreamKafkaMessageConverterTest.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/convert/WrapperRecordStreamKafkaMessageConverterTest.java
index 81295ba5144..ef135ceb077 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/convert/WrapperRecordStreamKafkaMessageConverterTest.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-processors/src/test/java/org/apache/nifi/kafka/processors/consumer/convert/WrapperRecordStreamKafkaMessageConverterTest.java
@@ -75,7 +75,7 @@ class WrapperRecordStreamKafkaMessageConverterTest {
                 readerFactory,
                 writerFactory,
                 keyReaderFactory,
-                StandardCharsets.UTF_8,
+                value -> new String(value, StandardCharsets.UTF_8),
                 Pattern.compile(".*"),
                 KeyFormat.STRING,
                 KeyEncoding.UTF8,
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-shared/src/main/java/org/apache/nifi/kafka/shared/property/KeyEncoding.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-shared/src/main/java/org/apache/nifi/kafka/shared/property/HeaderFormat.java
similarity index 71%
copy from 
nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-shared/src/main/java/org/apache/nifi/kafka/shared/property/KeyEncoding.java
copy to 
nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-shared/src/main/java/org/apache/nifi/kafka/shared/property/HeaderFormat.java
index a42b5275f2c..810fdc08099 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-shared/src/main/java/org/apache/nifi/kafka/shared/property/KeyEncoding.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-shared/src/main/java/org/apache/nifi/kafka/shared/property/HeaderFormat.java
@@ -19,18 +19,17 @@ package org.apache.nifi.kafka.shared.property;
 import org.apache.nifi.components.DescribedValue;
 
 /**
- * Enumeration of supported Kafka Key Encoding Strategies
+ * Enumeration of supported Kafka Record Header Value Formats
  */
-public enum KeyEncoding implements DescribedValue {
-    UTF8("utf-8", "UTF-8 Encoded", "The key is interpreted as a UTF-8 Encoded 
string."),
-    HEX("hex", "Hex Encoded", "The key is interpreted as arbitrary binary data 
and is encoded using hexadecimal characters with uppercase letters"),
-    DO_NOT_ADD("do-not-add", "Do Not Add Key as Attribute", "The key will not 
be added as an Attribute");
+public enum HeaderFormat implements DescribedValue {
+    STRING("string", "String", "The header value is decoded as a string using 
the configured Header Encoding character set."),
+    HEX("hex", "Hex Encoded", "The header value is interpreted as arbitrary 
binary data and is encoded as a lowercase hexadecimal string");
 
     private final String value;
     private final String displayName;
     private final String description;
 
-    KeyEncoding(final String value, final String displayName, final String 
description) {
+    HeaderFormat(final String value, final String displayName, final String 
description) {
         this.value = value;
         this.displayName = displayName;
         this.description = description;
diff --git 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-shared/src/main/java/org/apache/nifi/kafka/shared/property/KeyEncoding.java
 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-shared/src/main/java/org/apache/nifi/kafka/shared/property/KeyEncoding.java
index a42b5275f2c..68b79316593 100644
--- 
a/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-shared/src/main/java/org/apache/nifi/kafka/shared/property/KeyEncoding.java
+++ 
b/nifi-extension-bundles/nifi-kafka-bundle/nifi-kafka-shared/src/main/java/org/apache/nifi/kafka/shared/property/KeyEncoding.java
@@ -23,7 +23,7 @@ import org.apache.nifi.components.DescribedValue;
  */
 public enum KeyEncoding implements DescribedValue {
     UTF8("utf-8", "UTF-8 Encoded", "The key is interpreted as a UTF-8 Encoded 
string."),
-    HEX("hex", "Hex Encoded", "The key is interpreted as arbitrary binary data 
and is encoded using hexadecimal characters with uppercase letters"),
+    HEX("hex", "Hex Encoded", "The key is interpreted as arbitrary binary data 
and is encoded as a lowercase hexadecimal string"),
     DO_NOT_ADD("do-not-add", "Do Not Add Key as Attribute", "The key will not 
be added as an Attribute");
 
     private final String value;

Reply via email to