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

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

commit 6d66f0852a081d1391ad3ec3837f40c3ec93cf31
Author: smjain <[email protected]>
AuthorDate: Sat Oct 3 10:48:35 2026 +0530

    CAMEL-25305: camel-netty-http - write a String body in the charset that the 
Content-Type declares
    
    The consumer (response) and the producer (request) converted a String body
    with the charset of the exchange (UTF-8 by default), also when the
    Content-Type declared another charset, so for example a response with
    text/plain; charset=ISO-8859-1 carried UTF-8 bytes. A String body is now
    written in the declared charset.
    
    The charset of a request Content-Type was found with the deprecated
    HttpUtil.getCharsetFromContentType, which only matches the parameter name in
    lower case; the parameter name is case-insensitive (RFC 9110, section
    5.6.6).
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../netty/http/DefaultNettyHttpBinding.java        |  36 +++++-
 .../component/netty/http/NettyHttpConverter.java   |   3 +-
 .../component/netty/http/NettyHttpHelper.java      |  20 ++++
 .../http/handlers/HttpServerChannelHandler.java    |   3 +-
 .../netty/http/NettyHttpStringBodyCharsetTest.java | 121 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  10 ++
 6 files changed, 186 insertions(+), 7 deletions(-)

diff --git 
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/DefaultNettyHttpBinding.java
 
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/DefaultNettyHttpBinding.java
index 67e8d54704a8..dd73344dc665 100644
--- 
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/DefaultNettyHttpBinding.java
+++ 
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/DefaultNettyHttpBinding.java
@@ -280,6 +280,26 @@ public class DefaultNettyHttpBinding implements 
NettyHttpBinding, Cloneable {
 
     }
 
+    /**
+     * Converts a String body with the charset that the content type of the 
message 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 ByteBuf toByteBufWithContentTypeCharset(Message message, 
Object body) {
+        if (body instanceof String str) {
+            String charset = 
NettyHttpHelper.getCharsetFromContentType(MessageHelper.getContentType(message));
+            if (charset != null) {
+                try {
+                    return 
NettyConverter.toByteBuffer(str.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;
+    }
+
     private static String stripPath(NettyHttpConfiguration configuration, URI 
uri) {
         String path = uri.getRawPath();
         if (configuration.getPath() != null) {
@@ -493,8 +513,12 @@ public class DefaultNettyHttpBinding implements 
NettyHttpBinding, Cloneable {
             if (body instanceof ByteBuf byteBuf) {
                 buffer = byteBuf;
             } else {
-                // try to convert to buffer first
-                buffer = message.getBody(ByteBuf.class);
+                // a String body is written in the charset of the content type
+                buffer = toByteBufWithContentTypeCharset(message, body);
+                if (buffer == null) {
+                    // try to convert to buffer first
+                    buffer = message.getBody(ByteBuf.class);
+                }
                 if (buffer == null) {
                     // fallback to byte array as last resort
                     byte[] data = message.getBody(byte[].class);
@@ -652,8 +676,12 @@ public class DefaultNettyHttpBinding implements 
NettyHttpBinding, Cloneable {
                 if (body instanceof ByteBuf byteBuf) {
                     buffer = byteBuf;
                 } else {
-                    // try to convert to buffer first
-                    buffer = message.getBody(ByteBuf.class);
+                    // a String body is written in the charset of the content 
type
+                    buffer = toByteBufWithContentTypeCharset(message, body);
+                    if (buffer == null) {
+                        // try to convert to buffer first
+                        buffer = message.getBody(ByteBuf.class);
+                    }
                     if (buffer == null) {
                         // fallback to byte array as last resort
                         byte[] data = message.getMandatoryBody(byte[].class);
diff --git 
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpConverter.java
 
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpConverter.java
index 7ccd8cc8c278..f7afabf4bbdb 100644
--- 
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpConverter.java
+++ 
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpConverter.java
@@ -30,7 +30,6 @@ import org.apache.camel.Exchange;
 import org.apache.camel.ExchangePropertyKey;
 import org.apache.camel.component.netty.NettyConverter;
 import org.apache.camel.spi.TypeConverterRegistry;
-import org.apache.camel.support.http.HttpUtil;
 
 @Converter(generateLoader = true)
 public final class NettyHttpConverter {
@@ -88,7 +87,7 @@ public final class NettyHttpConverter {
     @Converter
     public static String toString(FullHttpResponse response, Exchange 
exchange) {
         String contentType = 
response.headers().get(NettyHttpConstants.CONTENT_TYPE);
-        String charset = HttpUtil.getCharsetFromContentType(contentType);
+        String charset = 
NettyHttpHelper.getCharsetFromContentType(contentType);
         if (charset == null && exchange != null) {
             charset = exchange.getProperty(ExchangePropertyKey.CHARSET_NAME, 
String.class);
         }
diff --git 
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpHelper.java
 
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpHelper.java
index 4ce470be55bd..9454acc599d4 100644
--- 
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpHelper.java
+++ 
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/NettyHttpHelper.java
@@ -56,6 +56,26 @@ public final class NettyHttpHelper {
         CollectionHelper.appendEntry(headers, key, value);
     }
 
+    /**
+     * Gets the charset parameter of the content type. The parameter name is 
case-insensitive (RFC 9110, section 5.6.6).
+     *
+     * @param  contentType the content type, may be <tt>null</tt>
+     * @return             the charset name, or <tt>null</tt> if the content 
type has no charset parameter
+     */
+    public static String getCharsetFromContentType(String contentType) {
+        if (contentType == null) {
+            return null;
+        }
+        String[] parts = contentType.split(";");
+        for (int i = 1; i < parts.length; i++) {
+            String part = parts[i].trim();
+            if (part.regionMatches(true, 0, "charset=", 0, 8)) {
+                return IOHelper.normalizeCharset(part.substring(8));
+            }
+        }
+        return null;
+    }
+
     /**
      * Creates the {@link HttpMethod} to use to call the remote server, often 
either its GET or POST.
      *
diff --git 
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandler.java
 
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandler.java
index 790ff8cc4903..9720f9ab2521 100644
--- 
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandler.java
+++ 
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandler.java
@@ -47,6 +47,7 @@ import 
org.apache.camel.component.netty.http.InboundStreamHttpRequest;
 import org.apache.camel.component.netty.http.NettyHttpConfiguration;
 import org.apache.camel.component.netty.http.NettyHttpConstants;
 import org.apache.camel.component.netty.http.NettyHttpConsumer;
+import org.apache.camel.component.netty.http.NettyHttpHelper;
 import org.apache.camel.component.netty.http.NettyHttpSecurityConfiguration;
 import org.apache.camel.component.netty.http.SecurityAuthenticator;
 import org.apache.camel.http.base.OAuthHttpSecuritySupport;
@@ -377,7 +378,7 @@ public class HttpServerChannelHandler extends 
ServerChannelHandler {
 
         // honor the character encoding
         String contentType = in.getHeader(NettyHttpConstants.CONTENT_TYPE, 
String.class);
-        String charset = 
org.apache.camel.support.http.HttpUtil.getCharsetFromContentType(contentType);
+        String charset = 
NettyHttpHelper.getCharsetFromContentType(contentType);
         if (charset != null) {
             exchange.setProperty(ExchangePropertyKey.CHARSET_NAME, charset);
             in.setHeader(NettyHttpConstants.HTTP_CHARACTER_ENCODING, charset);
diff --git 
a/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpStringBodyCharsetTest.java
 
b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpStringBodyCharsetTest.java
new file mode 100644
index 000000000000..727b17ab75c5
--- /dev/null
+++ 
b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpStringBodyCharsetTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.netty.http;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+
+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 org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * A String body is written in the charset that the Content-Type declares, and 
the charset parameter is recognized
+ * whatever its case.
+ */
+public class NettyHttpStringBodyCharsetTest extends BaseNettyTestSupport {
+
+    private static final String TEXT = "Grüße aus Köln";
+
+    @Test
+    public void testResponseInDeclaredCharset() throws Exception {
+        HttpResponse<byte[]> response = send("/response", "text/plain", 
TEXT.getBytes(StandardCharsets.UTF_8));
+
+        assertEquals("text/plain; charset=ISO-8859-1", 
response.headers().firstValue("Content-Type").orElse(null));
+        assertArrayEquals(TEXT.getBytes(StandardCharsets.ISO_8859_1), 
response.body());
+    }
+
+    @Test
+    public void testResponseWithoutCharsetIsUtf8() throws Exception {
+        HttpResponse<byte[]> response = send("/plain", "text/plain", 
TEXT.getBytes(StandardCharsets.UTF_8));
+
+        assertArrayEquals(TEXT.getBytes(StandardCharsets.UTF_8), 
response.body());
+    }
+
+    @Test
+    public void testRequestCharsetParameterIgnoresCase() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:text");
+        mock.expectedBodiesReceived(TEXT);
+
+        send("/text", "text/plain; Charset=ISO-8859-1", 
TEXT.getBytes(StandardCharsets.ISO_8859_1));
+
+        mock.assertIsSatisfied();
+    }
+
+    @Test
+    public void testProducerRequestInDeclaredCharset() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:bytes");
+        mock.expectedMessageCount(1);
+
+        
template.sendBodyAndHeader("netty-http:http://localhost:{{port}}/bytes";, TEXT, 
Exchange.CONTENT_TYPE,
+                "text/plain; charset=ISO-8859-1");
+
+        mock.assertIsSatisfied();
+        assertArrayEquals(TEXT.getBytes(StandardCharsets.ISO_8859_1),
+                
mock.getReceivedExchanges().get(0).getMessage().getBody(byte[].class));
+    }
+
+    @Test
+    public void testCharsetFromContentType() {
+        assertEquals("ISO-8859-1", 
NettyHttpHelper.getCharsetFromContentType("text/plain; Charset=ISO-8859-1"));
+        assertEquals("UTF-8", 
NettyHttpHelper.getCharsetFromContentType("text/plain;charset=\"UTF-8\""));
+        assertEquals("utf-16", 
NettyHttpHelper.getCharsetFromContentType("text/plain; format=flowed; 
CHARSET=utf-16"));
+        // no charset parameter: no charset (not a UTF-8 default), so the 
exchange charset is used as before
+        assertNull(NettyHttpHelper.getCharsetFromContentType("text/plain"));
+        
assertNull(NettyHttpHelper.getCharsetFromContentType("multipart/form-data; 
boundary=charset"));
+        assertNull(NettyHttpHelper.getCharsetFromContentType(null));
+    }
+
+    private HttpResponse<byte[]> send(String path, String contentType, byte[] 
body) throws Exception {
+        HttpRequest request = 
HttpRequest.newBuilder(URI.create("http://localhost:"; + getPort() + path))
+                .header("Content-Type", contentType)
+                .POST(HttpRequest.BodyPublishers.ofByteArray(body))
+                .build();
+        return HttpClient.newHttpClient().send(request, 
HttpResponse.BodyHandlers.ofByteArray());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("netty-http:http://localhost:{{port}}/response";)
+                        .setHeader(Exchange.CONTENT_TYPE, 
constant("text/plain; charset=ISO-8859-1"))
+                        .setBody(constant(TEXT));
+
+                from("netty-http:http://localhost:{{port}}/plain";)
+                        .setHeader(Exchange.CONTENT_TYPE, 
constant("text/plain"))
+                        .setBody(constant(TEXT));
+
+                from("netty-http:http://localhost:{{port}}/text";)
+                        .convertBodyTo(String.class)
+                        .to("mock:text");
+
+                from("netty-http:http://localhost:{{port}}/bytes";)
+                        .convertBodyTo(byte[].class)
+                        .to("mock:bytes");
+            }
+        };
+    }
+}
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 d65aa7a14049..4746ecf2b297 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
@@ -803,6 +803,16 @@ The `charset` parameter of a `Content-Type` was only 
recognized in lower case. A
 `String` body with such a `Content-Type` was written as UTF-8. The parameter 
name is now matched case-insensitively
 (RFC 9110), as in the other HTTP components, on the consumer (request and 
response) and on the producer.
 
+=== camel-netty-http - String bodies in the charset of the Content-Type
+
+The Netty HTTP consumer (response) and producer (request) wrote a `String` 
body in the charset of the exchange
+(`CamelCharsetName`, UTF-8 by default), even when the `Content-Type` declared 
another charset (for example
+`text/plain; charset=ISO-8859-1` set by the route). 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. Also, the `charset` parameter of a received `Content-Type` (a 
request on the consumer) is now
+recognized whatever its case (`Charset=ISO-8859-1`). 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 `?`.
+
 === camel-snmp - GET_NEXT (walk)
 
 A `GET_NEXT` walk now ends at the end of the agent's MIB view (`endOfMibView`, 
or `noSuchName` for SNMPv1) and at

Reply via email to