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 0466a57b30d1 CAMEL-25316: camel-http-common - write a String response
in the charset that the Content-Type declares (#27392)
0466a57b30d1 is described below
commit 0466a57b30d1462d9542a785fe7a4b91080b7dd1
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Oct 6 14:52:34 2026 +0530
CAMEL-25316: camel-http-common - write a String response in the charset
that the Content-Type declares (#27392)
The servlet and Jetty consumers converted a String response body to an input
stream with the charset of the exchange (UTF-8 unless the request named
another one) when the response is chunked, the default, and when it is not
chunked and its Content-Type is not text, for example application/json. The
Content-Type header kept the charset the route declared, so a response with
text/plain; charset=ISO-8859-1 carried UTF-8 bytes.
Convert a String body with the charset that the Content-Type declares, as
the
non-chunked text path already writes the body and the header in one charset.
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../camel/http/common/DefaultHttpBinding.java | 39 +++++++++-
.../servlet/ServletStringResponseCharsetTest.java | 82 ++++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 11 +++
3 files changed, 130 insertions(+), 2 deletions(-)
diff --git
a/components/camel-http-common/src/main/java/org/apache/camel/http/common/DefaultHttpBinding.java
b/components/camel-http-common/src/main/java/org/apache/camel/http/common/DefaultHttpBinding.java
index bbf974f1111f..64e1dbb23afb 100644
---
a/components/camel-http-common/src/main/java/org/apache/camel/http/common/DefaultHttpBinding.java
+++
b/components/camel-http-common/src/main/java/org/apache/camel/http/common/DefaultHttpBinding.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.http.common;
+import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
@@ -25,6 +26,7 @@ import java.io.PrintWriter;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
+import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
@@ -529,11 +531,19 @@ public class DefaultHttpBinding implements HttpBinding {
// prefer streaming
InputStream is = null;
if (checkChunked(message, exchange)) {
- is = message.getBody(InputStream.class);
+ // a String body is written in the charset of the content type
+ is = toInputStreamWithContentTypeCharset(message, contentType);
+ if (is == null) {
+ is = message.getBody(InputStream.class);
+ }
} else {
// try to use input stream first, so we can copy directly
if (!isText(contentType)) {
- is =
exchange.getContext().getTypeConverter().tryConvertTo(InputStream.class,
message.getBody());
+ // a String body is written in the charset of the content type
+ is = toInputStreamWithContentTypeCharset(message, contentType);
+ if (is == null) {
+ is =
exchange.getContext().getTypeConverter().tryConvertTo(InputStream.class,
message.getBody());
+ }
}
}
@@ -595,6 +605,31 @@ public class DefaultHttpBinding implements HttpBinding {
}
}
+ /**
+ * Converts a String body with the charset that the content type declares,
so the bytes match the Content-Type
+ * header. Returns <tt>null</tt> when the body is not a String or no
supported charset is declared, and the body is
+ * then converted with the type converter (the charset of the exchange).
+ */
+ private static InputStream toInputStreamWithContentTypeCharset(Message
message, String contentType) {
+ if (contentType != null && message.getBody() instanceof String text) {
+ for (String part : contentType.split(";")) {
+ part = part.trim();
+ // the parameter name is case-insensitive (RFC 9110, section
5.6.6)
+ if (part.regionMatches(true, 0, "charset=", 0, 8)) {
+ String charset =
IOHelper.normalizeCharset(part.substring(8));
+ try {
+ return new
ByteArrayInputStream(text.getBytes(Charset.forName(charset)));
+ } catch (IllegalArgumentException e) {
+ // unsupported or illegal charset name: keep the
conversion with the exchange charset
+ LOG.debug("Cannot use charset {} of the content type:
{}", charset, e.getMessage());
+ return null;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
protected boolean checkChunked(Message message, Exchange exchange) {
boolean answer = true;
if (message.getHeader(Exchange.HTTP_CHUNKED) == null) {
diff --git
a/components/camel-servlet/src/test/java/org/apache/camel/component/servlet/ServletStringResponseCharsetTest.java
b/components/camel-servlet/src/test/java/org/apache/camel/component/servlet/ServletStringResponseCharsetTest.java
new file mode 100644
index 000000000000..72214c3ca195
--- /dev/null
+++
b/components/camel-servlet/src/test/java/org/apache/camel/component/servlet/ServletStringResponseCharsetTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.servlet;
+
+import java.io.InputStream;
+import java.nio.charset.Charset;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.util.IOHelper;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * A String response body must be written in the charset that its Content-Type
declares, so a client that decodes the
+ * response with that charset gets the text back.
+ */
+class ServletStringResponseCharsetTest extends ServletCamelRouterTestSupport {
+
+ private static final String TEXT = "Grüße aus Köln";
+
+ @Test
+ void chunkedTextResponseInDeclaredCharset() throws Exception {
+ assertEquals(TEXT, queryAndDecode("/chunked"));
+ }
+
+ @Test
+ void jsonResponseInDeclaredCharset() throws Exception {
+ assertEquals("{\"greeting\":\"" + TEXT + "\"}",
queryAndDecode("/json"));
+ }
+
+ @Test
+ void notChunkedTextResponse() throws Exception {
+ assertEquals(TEXT, queryAndDecode("/notChunked"));
+ }
+
+ private String queryAndDecode(String path) throws Exception {
+ WebResponse response = query(new GetMethodWebRequest(contextUrl +
"/services" + path));
+ String contentType = response.getHeaderField("Content-Type");
+ String charset = IOHelper.getCharsetNameFromContentType(contentType);
+ assertNotNull(charset, "charset of the response Content-Type " +
contentType);
+ try (InputStream is = response.getInputStream()) {
+ return new String(is.readAllBytes(), Charset.forName(charset));
+ }
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("servlet:/chunked")
+ .setHeader(Exchange.CONTENT_TYPE,
constant("text/plain; charset=ISO-8859-1"))
+ .setBody(constant(TEXT));
+
+ from("servlet:/json?chunked=false")
+ .setHeader(Exchange.CONTENT_TYPE,
constant("application/json; charset=ISO-8859-1"))
+ .setBody(constant("{\"greeting\":\"" + TEXT + "\"}"));
+
+ from("servlet:/notChunked?chunked=false")
+ .setHeader(Exchange.CONTENT_TYPE,
constant("text/plain; charset=ISO-8859-1"))
+ .setBody(constant(TEXT));
+ }
+ };
+ }
+}
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 671d952fec02..d7961fa10ba7 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
@@ -851,6 +851,17 @@ Before, the first five fell back to the charset of the
exchange (UTF-8 by defaul
unmarshal sets `CamelCharsetName` to it. A message whose MSH-18 names one of
these charsets but that is actually
encoded in another one (such as UTF-8) is now decoded in the charset it names.
+=== camel-servlet and camel-jetty - String response in the charset of the
Content-Type
+
+The servlet and Jetty consumers converted a `String` response body with the
charset of the exchange (UTF-8 by
+default) when the response is chunked (the default), and also when it is not
chunked and its `Content-Type` is not
+text (for example `application/json`), even when the `Content-Type` declared
another charset (for example
+`text/plain; charset=ISO-8859-1`). Such a body is now written in the charset
that the `Content-Type` declares, so the
+bytes match the header. Responses without a charset in the `Content-Type`,
bodies that are not a `String`, and
+gzip-encoded responses (`Content-Encoding: gzip`) are written as before. A
client that ignored 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-platform-http-vertx - String response in the charset of the
Content-Type
A `String` response body was always written as UTF-8, even when the response
`Content-Type` declared another