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

Reply via email to