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