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 c7a96b7452fa CAMEL-25248: camel-undertow - write a String body in the
charset that the Content-Type declares (#27245)
c7a96b7452fa is described below
commit c7a96b7452fafdb13395fdedddd0d4e7f666fa7d
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Oct 2 12:55:49 2026 +0530
CAMEL-25248: camel-undertow - write a String body in the charset that the
Content-Type declares (#27245)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/component/undertow/UndertowConsumer.java | 12 +-
.../camel/component/undertow/UndertowHelper.java | 26 +++++
.../camel/component/undertow/UndertowProducer.java | 11 +-
.../undertow/UndertowStringBodyCharsetTest.java | 123 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 9 ++
5 files changed, 177 insertions(+), 4 deletions(-)
diff --git
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowConsumer.java
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowConsumer.java
index 0cd70ca89b78..7df12a1e2ce2 100644
---
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowConsumer.java
+++
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowConsumer.java
@@ -288,8 +288,16 @@ public class UndertowConsumer extends DefaultConsumer
implements HttpHandler, Su
IOHelper.copy(input, output, IOHelper.DEFAULT_BUFFER_SIZE,
true);
}
} else {
- TypeConverter tc =
getEndpoint().getCamelContext().getTypeConverter();
- ByteBuffer bodyAsByteBuffer =
tc.mandatoryConvertTo(ByteBuffer.class, body);
+ ByteBuffer bodyAsByteBuffer = null;
+ if (body instanceof String text) {
+ // write the text in the charset that the response
Content-Type declares, if any
+ bodyAsByteBuffer = UndertowHelper.toByteBuffer(text,
+
httpExchange.getResponseHeaders().getFirst(Headers.CONTENT_TYPE));
+ }
+ if (bodyAsByteBuffer == null) {
+ TypeConverter tc =
getEndpoint().getCamelContext().getTypeConverter();
+ bodyAsByteBuffer = tc.mandatoryConvertTo(ByteBuffer.class,
body);
+ }
httpExchange.getResponseSender().send(bodyAsByteBuffer);
}
}
diff --git
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowHelper.java
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowHelper.java
index 460d12a30664..81035bbe7175 100644
---
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowHelper.java
+++
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowHelper.java
@@ -18,12 +18,16 @@ package org.apache.camel.component.undertow;
import java.net.URI;
import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
import java.util.Map;
+import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
import org.apache.camel.Exchange;
import org.apache.camel.util.CollectionHelper;
+import org.apache.camel.util.IOHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.URISupport;
import org.apache.camel.util.UnsafeUriCharactersEncoder;
@@ -174,4 +178,26 @@ public final class UndertowHelper {
}
}
+ /**
+ * Encodes a String body to send over HTTP in the charset that the
Content-Type of the message declares, so that the
+ * bytes match the header.
+ *
+ * @param body the String body
+ * @param contentType the Content-Type of the request or response being
sent, may be <tt>null</tt>
+ * @return the encoded body, or <tt>null</tt> when the
Content-Type declares no charset or one that is
+ * not supported, in which case the body is converted
as before (UTF-8 by default)
+ */
+ public static ByteBuffer toByteBuffer(String body, String contentType) {
+ String name = contentType != null ?
Headers.extractQuotedValueFromHeader(contentType, "charset") : null;
+ if (ObjectHelper.isEmpty(name)) {
+ return null;
+ }
+ try {
+ return
ByteBuffer.wrap(body.getBytes(Charset.forName(IOHelper.normalizeCharset(name))));
+ } catch (IllegalArgumentException e) {
+ // unknown or unsupported charset in the Content-Type
+ return null;
+ }
+ }
+
}
diff --git
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowProducer.java
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowProducer.java
index 0d053cffb241..151e564ad26e 100644
---
a/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowProducer.java
+++
b/components/camel-undertow/src/main/java/org/apache/camel/component/undertow/UndertowProducer.java
@@ -152,8 +152,15 @@ public class UndertowProducer extends DefaultAsyncProducer
{
camelExchange, callback, getEndpoint(),
request, (InputStream) body);
} else {
- final TypeConverter tc =
endpoint.getCamelContext().getTypeConverter();
- final ByteBuffer bodyAsByte = tc.tryConvertTo(ByteBuffer.class,
body);
+ ByteBuffer bodyAsByte = null;
+ if (body instanceof String text) {
+ // write the text in the charset that the request Content-Type
declares, if any
+ bodyAsByte = UndertowHelper.toByteBuffer(text,
requestHeaders.getFirst(Headers.CONTENT_TYPE));
+ }
+ if (bodyAsByte == null) {
+ final TypeConverter tc =
endpoint.getCamelContext().getTypeConverter();
+ bodyAsByte = tc.tryConvertTo(ByteBuffer.class, body);
+ }
// As tryConvertTo is used to convert the body, we should do null
check
// or the call bodyAsByte.remaining() may throw an NPE
diff --git
a/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowStringBodyCharsetTest.java
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowStringBodyCharsetTest.java
new file mode 100644
index 000000000000..0700b891588b
--- /dev/null
+++
b/components/camel-undertow/src/test/java/org/apache/camel/component/undertow/UndertowStringBodyCharsetTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.component.undertow;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.jupiter.api.Test;
+
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A String body is written in the charset that the Content-Type declares,
otherwise as UTF-8.
+ */
+public class UndertowStringBodyCharsetTest extends BaseUndertowTest {
+
+ private static final String TEXT = "Grüße aus Köln";
+ private static final String LATIN1 = "text/plain; charset=ISO-8859-1";
+
+ @Test
+ public void testResponseUsesCharsetOfContentType() throws Exception {
+ HttpResponse<byte[]> response =
send(HttpRequest.newBuilder(uri("latin1")).GET().build());
+
+ assertEquals(LATIN1,
response.headers().firstValue("Content-Type").orElse(null));
+ assertArrayEquals(TEXT.getBytes(ISO_8859_1), response.body());
+ }
+
+ @Test
+ public void testEchoUsesCharsetOfRequest() throws Exception {
+ HttpResponse<byte[]> response =
send(HttpRequest.newBuilder(uri("echo"))
+ .header("Content-Type", LATIN1)
+
.POST(HttpRequest.BodyPublishers.ofByteArray(TEXT.getBytes(ISO_8859_1))).build());
+
+ assertArrayEquals(TEXT.getBytes(ISO_8859_1), response.body());
+ }
+
+ @Test
+ public void testResponseWithoutCharsetIsUtf8() throws Exception {
+ HttpResponse<byte[]> response =
send(HttpRequest.newBuilder(uri("utf8")).GET().build());
+
+ assertArrayEquals(TEXT.getBytes(UTF_8), response.body());
+ }
+
+ @Test
+ public void testResponseWithoutCharsetToLatin1RequestIsUtf8() throws
Exception {
+ // the request charset (CamelCharsetName) must not be used for a
response that does not declare it
+ HttpResponse<byte[]> response =
send(HttpRequest.newBuilder(uri("noCharset"))
+ .header("Content-Type", LATIN1)
+
.POST(HttpRequest.BodyPublishers.ofByteArray(TEXT.getBytes(ISO_8859_1))).build());
+
+ assertEquals("text/plain",
response.headers().firstValue("Content-Type").orElse(null));
+ assertArrayEquals(TEXT.getBytes(UTF_8), response.body());
+ }
+
+ @Test
+ public void testProducerUsesCharsetOfContentType() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:received");
+ mock.expectedMessageCount(1);
+
+
template.sendBodyAndHeader("undertow:http://localhost:{{port}}/received", TEXT,
Exchange.CONTENT_TYPE, LATIN1);
+
+ MockEndpoint.assertIsSatisfied(context);
+ assertArrayEquals(TEXT.getBytes(ISO_8859_1),
mock.getExchanges().get(0).getIn().getBody(byte[].class));
+ }
+
+ private URI uri(String path) {
+ return URI.create("http://localhost:" + getPort() + "/" + path);
+ }
+
+ private static HttpResponse<byte[]> send(HttpRequest request) throws
Exception {
+ return HttpClient.newHttpClient().send(request,
HttpResponse.BodyHandlers.ofByteArray());
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("undertow:http://localhost:{{port}}/latin1")
+ .setHeader(Exchange.CONTENT_TYPE, constant(LATIN1))
+ .setBody(constant(TEXT));
+
+ from("undertow:http://localhost:{{port}}/echo")
+ .convertBodyTo(String.class);
+
+ from("undertow:http://localhost:{{port}}/noCharset")
+ .convertBodyTo(String.class)
+ .setHeader(Exchange.CONTENT_TYPE,
constant("text/plain"));
+
+ from("undertow:http://localhost:{{port}}/utf8")
+ .setHeader(Exchange.CONTENT_TYPE,
constant("text/plain"))
+ .setBody(constant(TEXT));
+
+ from("undertow:http://localhost:{{port}}/received")
+ .convertBodyTo(byte[].class)
+ .to("mock:received")
+ .setBody(constant("OK"));
+ }
+ };
+ }
+}
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 2f36e4a00e02..1800adfbabfd 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
@@ -737,6 +737,15 @@ in the charset that the `Content-Type` declares, so the
bytes match the header.
the declared charset and read such a response as UTF-8 must now use the
declared charset, and characters that the
declared charset cannot represent are written as `?`.
+=== camel-undertow - String bodies in the charset of the Content-Type
+
+The Undertow consumer (response) and producer (request) always wrote a
`String` body as UTF-8, even when the
+`Content-Type` declared another charset (for example `text/plain;
charset=ISO-8859-1`). A `String` body is now
+written in the charset that the `Content-Type` declares. Bodies that are not a
`String`, and messages whose
+`Content-Type` declares no charset, are sent as before (UTF-8 by default). A
peer that ignored the declared charset and
+read such a message as UTF-8 must now use the declared charset, and characters
that the declared charset cannot
+represent are written as `?`.
+
=== Components and Language removal
==== camel-csimple, camel-csimple-joor and csimple-maven-plugin