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

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


The following commit(s) were added to refs/heads/main by this push:
     new e60d605a31d9 CAMEL-25034: camel-support - convertBodyTo with a charset 
must use it when the message has a charset header (#26909)
e60d605a31d9 is described below

commit e60d605a31d9245d7251b85bdeedcfc1abbfebbc
Author: allthingssecurity <[email protected]>
AuthorDate: Mon Sep 28 13:30:23 2026 +0530

    CAMEL-25034: camel-support - convertBodyTo with a charset must use it when 
the message has a charset header (#26909)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../processor/converter/ConvertCharsetTest.java    | 134 +++++++++++++++++++++
 .../support/processor/ConvertBodyProcessor.java    |  39 +++---
 .../support/processor/ConvertHeaderProcessor.java  |  48 +++++---
 .../processor/ConvertVariableProcessor.java        |  39 +++---
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   9 ++
 5 files changed, 223 insertions(+), 46 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/converter/ConvertCharsetTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/converter/ConvertCharsetTest.java
new file mode 100644
index 000000000000..c0a87810c9bd
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/converter/ConvertCharsetTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.camel.processor.converter;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The charset configured on convertBodyTo, convertHeaderTo and 
convertVariableTo is used even when the message has a
+ * charset header, and the exchange charset is restored also when the 
conversion fails.
+ */
+class ConvertCharsetTest extends ContextTestSupport {
+
+    private static final byte[] CAFE = "café".getBytes(StandardCharsets.UTF_8);
+
+    @Test
+    void testConvertBodyWithCharsetHeader() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedBodiesReceived("café");
+        mock.expectedHeaderReceived(Exchange.CHARSET_NAME, "ISO-8859-1");
+        mock.message(0).exchangeProperty(Exchange.CHARSET_NAME).isNull();
+
+        template.sendBodyAndHeader("direct:body", CAFE, Exchange.CHARSET_NAME, 
"ISO-8859-1");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    void testConvertHeaderWithCharsetHeader() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedHeaderReceived("data", "café");
+        mock.expectedHeaderReceived(Exchange.CHARSET_NAME, "ISO-8859-1");
+        mock.message(0).exchangeProperty(Exchange.CHARSET_NAME).isNull();
+
+        template.send("direct:header", e -> {
+            e.getMessage().setHeader("data", CAFE);
+            e.getMessage().setHeader(Exchange.CHARSET_NAME, "ISO-8859-1");
+        });
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    void testConvertVariableWithCharsetHeader() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedVariableReceived("data", "café");
+        mock.expectedHeaderReceived(Exchange.CHARSET_NAME, "ISO-8859-1");
+        mock.message(0).exchangeProperty(Exchange.CHARSET_NAME).isNull();
+
+        template.send("direct:variable", e -> {
+            e.setVariable("data", CAFE);
+            e.getMessage().setHeader(Exchange.CHARSET_NAME, "ISO-8859-1");
+        });
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    void testConvertBodyFailedRestoresCharset() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:error");
+        mock.expectedMessageCount(2);
+        mock.message(0).exchangeProperty(Exchange.CHARSET_NAME).isNull();
+        
mock.message(1).exchangeProperty(Exchange.CHARSET_NAME).isEqualTo("ISO-8859-1");
+        mock.message(1).header(Exchange.CHARSET_NAME).isEqualTo("ISO-8859-1");
+
+        template.sendBody("direct:bodyFail", "abc");
+        template.send("direct:bodyFail", e -> {
+            e.setProperty(Exchange.CHARSET_NAME, "ISO-8859-1");
+            e.getMessage().setHeader(Exchange.CHARSET_NAME, "ISO-8859-1");
+            e.getMessage().setBody("abc");
+        });
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    void testConvertHeaderFailedRestoresCharset() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:error");
+        mock.expectedMessageCount(1);
+        mock.message(0).exchangeProperty(Exchange.CHARSET_NAME).isNull();
+
+        template.sendBodyAndHeader("direct:headerFail", "Hello", "data", 
"abc");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    void testConvertVariableFailedRestoresCharset() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:error");
+        mock.expectedMessageCount(1);
+        mock.message(0).exchangeProperty(Exchange.CHARSET_NAME).isNull();
+
+        template.send("direct:variableFail", e -> e.setVariable("data", 
"abc"));
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                onException(Exception.class).handled(true).to("mock:error");
+
+                from("direct:body").convertBodyTo(String.class, 
"UTF-8").to("mock:result");
+                from("direct:header").convertHeaderTo("data", String.class, 
"UTF-8").to("mock:result");
+                from("direct:variable").convertVariableTo("data", 
String.class, "UTF-8").to("mock:result");
+
+                from("direct:bodyFail").convertBodyTo(Integer.class, 
"UTF-16").to("mock:result");
+                from("direct:headerFail").convertHeaderTo("data", 
Integer.class, "UTF-16").to("mock:result");
+                from("direct:variableFail").convertVariableTo("data", 
Integer.class, "UTF-16").to("mock:result");
+            }
+        };
+    }
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/processor/ConvertBodyProcessor.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/ConvertBodyProcessor.java
index 008c5c627c57..a6b31e849eae 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/processor/ConvertBodyProcessor.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/ConvertBodyProcessor.java
@@ -131,18 +131,39 @@ public class ConvertBodyProcessor extends ServiceSupport
         }
 
         String originalCharsetName = null;
+        Object originalCharsetHeader = null;
         if (charset != null) {
             originalCharsetName = 
exchange.getProperty(ExchangePropertyKey.CHARSET_NAME, String.class);
             // override existing charset with configured charset as that is 
what the user
             // have explicit configured and expects to be used
             exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, charset);
+            // the charset header takes precedence over the property, so 
override it as well while converting
+            originalCharsetHeader = 
exchange.getIn().getHeader(Exchange.CHARSET_NAME);
+            if (originalCharsetHeader != null) {
+                exchange.getIn().setHeader(Exchange.CHARSET_NAME, charset);
+            }
         }
         // use mandatory conversion
         Object value;
-        if (mandatory) {
-            value = old.getMandatoryBody(type);
-        } else {
-            value = old.getBody(type);
+        try {
+            if (mandatory) {
+                value = old.getMandatoryBody(type);
+            } else {
+                value = old.getBody(type);
+            }
+        } finally {
+            // remove or restore charset when we are done (also if the 
conversion failed) as we should not
+            // propagate that, as that can lead to double converting later on
+            if (charset != null) {
+                if (originalCharsetHeader != null) {
+                    exchange.getIn().setHeader(Exchange.CHARSET_NAME, 
originalCharsetHeader);
+                }
+                if (originalCharsetName != null && 
!originalCharsetName.isEmpty()) {
+                    exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, 
originalCharsetName);
+                } else {
+                    exchange.removeProperty(ExchangePropertyKey.CHARSET_NAME);
+                }
+            }
         }
 
         // create a new message container so we do not drag specialized 
message objects along
@@ -159,16 +180,6 @@ public class ConvertBodyProcessor extends ServiceSupport
             // no copy needed so set replace value directly
             old.setBody(value);
         }
-
-        // remove or restore charset when we are done as we should not 
propagate that,
-        // as that can lead to double converting later on
-        if (charset != null) {
-            if (originalCharsetName != null && !originalCharsetName.isEmpty()) 
{
-                exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, 
originalCharsetName);
-            } else {
-                exchange.removeProperty(ExchangePropertyKey.CHARSET_NAME);
-            }
-        }
     }
 
     @Override
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/processor/ConvertHeaderProcessor.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/ConvertHeaderProcessor.java
index b600af29371e..daf2512c01d6 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/processor/ConvertHeaderProcessor.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/ConvertHeaderProcessor.java
@@ -129,34 +129,46 @@ public class ConvertHeaderProcessor extends ServiceSupport
             return;
         }
 
-        String originalCharsetName = null;
-        if (charset != null) {
-            originalCharsetName = 
exchange.getProperty(ExchangePropertyKey.CHARSET_NAME, String.class);
-            // override existing charset with configured charset as that is 
what the user
-            // have explicit configured and expects to be used
-            exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, charset);
-        }
         // use mandatory conversion
         Object value = old.getHeader(name);
         if (value == null && mandatory) {
             throw new NoSuchHeaderOrPropertyException(exchange, name, null, 
type);
         }
-        if (mandatory) {
-            value = 
exchange.getContext().getTypeConverter().mandatoryConvertTo(type, exchange, 
value);
-        } else {
-            value = exchange.getContext().getTypeConverter().convertTo(type, 
exchange, value);
-        }
-        old.setHeader(targetName, value);
 
-        // remove or restore charset when we are done as we should not 
propagate that,
-        // as that can lead to double converting later on
+        String originalCharsetName = null;
+        Object originalCharsetHeader = null;
         if (charset != null) {
-            if (originalCharsetName != null && !originalCharsetName.isEmpty()) 
{
-                exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, 
originalCharsetName);
+            originalCharsetName = 
exchange.getProperty(ExchangePropertyKey.CHARSET_NAME, String.class);
+            // override existing charset with configured charset as that is 
what the user
+            // have explicit configured and expects to be used
+            exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, charset);
+            // the charset header takes precedence over the property, so 
override it as well while converting
+            originalCharsetHeader = 
exchange.getIn().getHeader(Exchange.CHARSET_NAME);
+            if (originalCharsetHeader != null) {
+                exchange.getIn().setHeader(Exchange.CHARSET_NAME, charset);
+            }
+        }
+        try {
+            if (mandatory) {
+                value = 
exchange.getContext().getTypeConverter().mandatoryConvertTo(type, exchange, 
value);
             } else {
-                exchange.removeProperty(ExchangePropertyKey.CHARSET_NAME);
+                value = 
exchange.getContext().getTypeConverter().convertTo(type, exchange, value);
+            }
+        } finally {
+            // remove or restore charset when we are done (also if the 
conversion failed) as we should not
+            // propagate that, as that can lead to double converting later on
+            if (charset != null) {
+                if (originalCharsetHeader != null) {
+                    exchange.getIn().setHeader(Exchange.CHARSET_NAME, 
originalCharsetHeader);
+                }
+                if (originalCharsetName != null && 
!originalCharsetName.isEmpty()) {
+                    exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, 
originalCharsetName);
+                } else {
+                    exchange.removeProperty(ExchangePropertyKey.CHARSET_NAME);
+                }
             }
         }
+        old.setHeader(targetName, value);
     }
 
     @Override
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/processor/ConvertVariableProcessor.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/ConvertVariableProcessor.java
index 959191ec3de3..c439d70112dd 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/processor/ConvertVariableProcessor.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/ConvertVariableProcessor.java
@@ -155,16 +155,37 @@ public class ConvertVariableProcessor extends 
ServiceSupport
         }
 
         String originalCharsetName = null;
+        Object originalCharsetHeader = null;
         if (charset != null) {
             originalCharsetName = 
exchange.getProperty(ExchangePropertyKey.CHARSET_NAME, String.class);
             // override existing charset with configured charset as that is 
what the user
             // have explicit configured and expects to be used
             exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, charset);
+            // the charset header takes precedence over the property, so 
override it as well while converting
+            originalCharsetHeader = 
exchange.getIn().getHeader(Exchange.CHARSET_NAME);
+            if (originalCharsetHeader != null) {
+                exchange.getIn().setHeader(Exchange.CHARSET_NAME, charset);
+            }
         }
-        if (mandatory) {
-            value = 
exchange.getContext().getTypeConverter().mandatoryConvertTo(type, exchange, 
value);
-        } else {
-            value = exchange.getContext().getTypeConverter().convertTo(type, 
exchange, value);
+        try {
+            if (mandatory) {
+                value = 
exchange.getContext().getTypeConverter().mandatoryConvertTo(type, exchange, 
value);
+            } else {
+                value = 
exchange.getContext().getTypeConverter().convertTo(type, exchange, value);
+            }
+        } finally {
+            // remove or restore charset when we are done (also if the 
conversion failed) as we should not
+            // propagate that, as that can lead to double converting later on
+            if (charset != null) {
+                if (originalCharsetHeader != null) {
+                    exchange.getIn().setHeader(Exchange.CHARSET_NAME, 
originalCharsetHeader);
+                }
+                if (originalCharsetName != null && 
!originalCharsetName.isEmpty()) {
+                    exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, 
originalCharsetName);
+                } else {
+                    exchange.removeProperty(ExchangePropertyKey.CHARSET_NAME);
+                }
+            }
         }
 
         if (repo != null) {
@@ -172,16 +193,6 @@ public class ConvertVariableProcessor extends 
ServiceSupport
         } else {
             exchange.setVariable(targetName, value);
         }
-
-        // remove or restore charset when we are done as we should not 
propagate that,
-        // as that can lead to double converting later on
-        if (charset != null) {
-            if (originalCharsetName != null && !originalCharsetName.isEmpty()) 
{
-                exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, 
originalCharsetName);
-            } else {
-                exchange.removeProperty(ExchangePropertyKey.CHARSET_NAME);
-            }
-        }
     }
 
     @Override
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 7c178afb2631..7f8a3c26da79 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -131,6 +131,15 @@ Prior to Camel 4.23 the property was only set when there 
was no fallback and was
 so a fallback that tested it for `null` must now test for `true` or `false` 
instead.
 `CamelCircuitBreakerResponseShortCircuited` is unchanged and remains `true` 
whenever the fallback runs, whatever the cause.
 
+=== Convert Body, Convert Header and Convert Variable EIPs
+
+When a charset is configured, such as `convertBodyTo(String.class, "UTF-8")`, 
then the conversion now uses that charset
+also when the message has a `CamelCharsetName` header. Previously the header 
took precedence, so the configured charset
+was ignored. The header is kept on the message unchanged.
+
+The `CamelCharsetName` exchange property is now also restored when the 
conversion fails. Previously the configured charset
+was left on the exchange, where the error handler would use it.
+
 === Error handler - onException when the exception changes during redelivery
 
 When a redelivery attempt fails with a different exception than the previous 
attempt, the error handler now uses

Reply via email to