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 fe77c8e2bc3e CAMEL-26144: camel-cxfrs - reply with an unchanged 
request body and send a whole List body (#28923)
fe77c8e2bc3e is described below

commit fe77c8e2bc3ea582f4c2d98b7d0d516822ee1a72
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:47:34 2026 +0100

    CAMEL-26144: camel-cxfrs - reply with an unchanged request body and send a 
whole List body (#28923)
    
    * CAMEL-26144: camel-cxfrs - reply with an unchanged request body and s…
    
    …end a whole List body
    
    With the Default binding a route that did not set a reply body replied with
    the request MessageContentsList, which failed with HTTP 500 "No message body
    writer has been found". Now a MessageContentsList of one element is the
    response entity and an empty one replies with no content, unless the 
resource
    method returns a collection. The Simple binding is unchanged.
    
    The HTTP API producer sent only the first element of a List body. Now a List
    is sent as a whole; of a MessageContentsList the first element is still 
sent,
    and an empty one sends no body instead of failing.
    
    * CAMEL-26144: camel-cxfrs - send a primitive array body as a whole
    
    The HTTP API producer cast any array body other than byte[] to Object[] to
    take its first element, so a primitive array such as int[] failed with a
    ClassCastException. Now a primitive array is sent as a whole, like byte[].
    An Object[] still sends its first element, as a MessageContentsList does,
    and the docs say so.
    
    * CAMEL-26144: camel-cxfrs - document that a MessageContentsList of sev…
    
    …eral parameters still fails
    
    The component page and the upgrade guide now say that an unchanged request 
MessageContentsList of
    more than one parameter still fails with HTTP 500, and the route must set 
the reply body.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/components/cxfrs.json |  4 +-
 .../apache/camel/catalog/docs/cxfrs-component.adoc | 11 ++-
 .../apache/camel/component/cxf/jaxrs/cxfrs.json    |  4 +-
 .../src/main/docs/cxfrs-component.adoc             | 11 ++-
 .../camel/component/cxf/jaxrs/CxfRsEndpoint.java   | 12 ++-
 .../component/cxf/jaxrs/DefaultCxfRsBinding.java   | 40 +++++++--
 .../component/cxf/jaxrs/SimpleCxfRsBinding.java    |  3 +-
 .../cxf/jaxrs/CxfRsConsumerUnchangedBodyTest.java  | 99 ++++++++++++++++++++++
 .../cxf/jaxrs/DefaultCxfRsBindingTest.java         | 89 +++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    | 15 ++++
 10 files changed, 268 insertions(+), 20 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cxfrs.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cxfrs.json
index abaef59b426f..e92f191a6a67 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cxfrs.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cxfrs.json
@@ -52,8 +52,8 @@
   },
   "bodyTypes": {
     "consumerOut": { "carriers": [ "list" ], "javaType": 
"org.apache.cxf.message.MessageContentsList", "dependsOn": [ 
"performInvocation" ], "byOption": [ { "option": "bindingStyle", "value": 
"SimpleConsumer", "carriers": [ "unknown" ] }, { "option": "bindingStyle", 
"value": "Custom", "carriers": [ "unknown" ] } ], "description": "Default: a 
MessageContentsList of the parameters of the resource method, with its name in 
the CamelCxfOperationName header. SimpleConsumer: the request entity  [...]
-    "consumerReply": { "carriers": [ "null", "text", "bytes", "stream", 
"file", "pojo" ], "byOption": [ { "option": "bindingStyle", "value": "Custom", 
"carriers": [ "any" ] } ], "description": "The response entity: a 
jakarta.ws.rs.core.Response, or text, bytes, a stream, a file, or an object a 
JAX-RS provider writes for the Content-Type (a JAXB bean as XML); a Map or a 
List fails without a provider for it. With the Default binding the request 
MessageContentsList fails: set the reply body" },
-    "producerIn": { "carriers": [ "null", "text", "bytes", "stream", "file", 
"pojo" ], "byOption": [ { "option": "httpClientAPI", "value": "false", 
"carriers": [ "list", "text", "value", "pojo", "null" ] } ], "description": 
"The request entity, written for the Content-Type header: text, bytes, a 
stream, a file or an object a JAX-RS provider writes; of a List only the first 
element is sent, and a Map fails. A GET sends no body. With 
httpClientAPI=false: the parameters of the resource meth [...]
+    "consumerReply": { "carriers": [ "null", "text", "bytes", "stream", 
"file", "pojo" ], "byOption": [ { "option": "bindingStyle", "value": "Custom", 
"carriers": [ "any" ] } ], "description": "The response entity: a 
jakarta.ws.rs.core.Response, or text, bytes, a stream, a file, or an object a 
JAX-RS provider writes for the Content-Type (a JAXB bean as XML); a Map or a 
List fails without a provider for it. With the Default binding an unchanged 
request MessageContentsList replies with its [...]
+    "producerIn": { "carriers": [ "null", "text", "bytes", "stream", "file", 
"pojo" ], "byOption": [ { "option": "httpClientAPI", "value": "false", 
"carriers": [ "list", "text", "value", "pojo", "null" ] } ], "description": 
"The request entity, written for the Content-Type header: text, bytes, a 
stream, a file or an object a JAX-RS provider writes, such as a List as a whole 
(of a MessageContentsList or an Object[] only the first element is sent); a Map 
fails. A GET sends no body. With ht [...]
     "producerOut": { "carriers": [ "pojo" ], "byOption": [ { "option": 
"httpClientAPI", "value": "false", "carriers": [ "unknown" ] } ], 
"description": "A jakarta.ws.rs.core.Response whose entity is the response 
stream, read once (it converts to a String or an InputStream); the entity read 
as the class in the CamelCxfRsResponseClass header when that is set. With 
httpClientAPI=false: the return value of the resource method, null for a void 
one" }
   },
   "properties": {
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cxfrs-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cxfrs-component.adoc
index 94a2892c2e33..b67f7f19e031 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cxfrs-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/cxfrs-component.adoc
@@ -101,7 +101,11 @@ The `Default` binding style is rather low-level, requiring 
the user to
 manually process the `MessageContentsList` object coming into the route.
 Thus, it tightly couples the route logic with the method signature and
 parameter indices of the JAX-RS operation. Somewhat inelegant, difficult
-and error-prone.
+and error-prone. When a route with the `Default` binding style does not set
+a reply body, the request `MessageContentsList` of one parameter is the
+response entity, and one without parameters replies with no content (204),
+unless the resource method returns a collection. A `MessageContentsList` of
+several parameters fails with HTTP 500, so set the reply body in the route.
 
 In contrast, the `SimpleConsumer` binding style performs the following
 mappings, to *make the request data more accessible* to you
@@ -425,6 +429,11 @@ let the producer use the http centric client API by using 
the URI option
 You can turn the response object to the type class specified with the
 message
 header `CamelCxfRsResponseClass`.
+The message body is the request entity (a GET sends none): a `List` or an
+array of primitives (such as `byte[]` or `int[]`) is sent as a whole, and of a
+`MessageContentsList` (such as the body from a `cxfrs` consumer with the
+`Default` binding style) or an `Object[]` only the first element is sent, as
+they carry the parameters of a method call.
 
 ._Java-only: Java test API (ProducerTemplate with inline Processor)_
 [source,java]
diff --git 
a/components/camel-cxf/camel-cxf-rest/src/generated/resources/META-INF/org/apache/camel/component/cxf/jaxrs/cxfrs.json
 
b/components/camel-cxf/camel-cxf-rest/src/generated/resources/META-INF/org/apache/camel/component/cxf/jaxrs/cxfrs.json
index abaef59b426f..e92f191a6a67 100644
--- 
a/components/camel-cxf/camel-cxf-rest/src/generated/resources/META-INF/org/apache/camel/component/cxf/jaxrs/cxfrs.json
+++ 
b/components/camel-cxf/camel-cxf-rest/src/generated/resources/META-INF/org/apache/camel/component/cxf/jaxrs/cxfrs.json
@@ -52,8 +52,8 @@
   },
   "bodyTypes": {
     "consumerOut": { "carriers": [ "list" ], "javaType": 
"org.apache.cxf.message.MessageContentsList", "dependsOn": [ 
"performInvocation" ], "byOption": [ { "option": "bindingStyle", "value": 
"SimpleConsumer", "carriers": [ "unknown" ] }, { "option": "bindingStyle", 
"value": "Custom", "carriers": [ "unknown" ] } ], "description": "Default: a 
MessageContentsList of the parameters of the resource method, with its name in 
the CamelCxfOperationName header. SimpleConsumer: the request entity  [...]
-    "consumerReply": { "carriers": [ "null", "text", "bytes", "stream", 
"file", "pojo" ], "byOption": [ { "option": "bindingStyle", "value": "Custom", 
"carriers": [ "any" ] } ], "description": "The response entity: a 
jakarta.ws.rs.core.Response, or text, bytes, a stream, a file, or an object a 
JAX-RS provider writes for the Content-Type (a JAXB bean as XML); a Map or a 
List fails without a provider for it. With the Default binding the request 
MessageContentsList fails: set the reply body" },
-    "producerIn": { "carriers": [ "null", "text", "bytes", "stream", "file", 
"pojo" ], "byOption": [ { "option": "httpClientAPI", "value": "false", 
"carriers": [ "list", "text", "value", "pojo", "null" ] } ], "description": 
"The request entity, written for the Content-Type header: text, bytes, a 
stream, a file or an object a JAX-RS provider writes; of a List only the first 
element is sent, and a Map fails. A GET sends no body. With 
httpClientAPI=false: the parameters of the resource meth [...]
+    "consumerReply": { "carriers": [ "null", "text", "bytes", "stream", 
"file", "pojo" ], "byOption": [ { "option": "bindingStyle", "value": "Custom", 
"carriers": [ "any" ] } ], "description": "The response entity: a 
jakarta.ws.rs.core.Response, or text, bytes, a stream, a file, or an object a 
JAX-RS provider writes for the Content-Type (a JAXB bean as XML); a Map or a 
List fails without a provider for it. With the Default binding an unchanged 
request MessageContentsList replies with its [...]
+    "producerIn": { "carriers": [ "null", "text", "bytes", "stream", "file", 
"pojo" ], "byOption": [ { "option": "httpClientAPI", "value": "false", 
"carriers": [ "list", "text", "value", "pojo", "null" ] } ], "description": 
"The request entity, written for the Content-Type header: text, bytes, a 
stream, a file or an object a JAX-RS provider writes, such as a List as a whole 
(of a MessageContentsList or an Object[] only the first element is sent); a Map 
fails. A GET sends no body. With ht [...]
     "producerOut": { "carriers": [ "pojo" ], "byOption": [ { "option": 
"httpClientAPI", "value": "false", "carriers": [ "unknown" ] } ], 
"description": "A jakarta.ws.rs.core.Response whose entity is the response 
stream, read once (it converts to a String or an InputStream); the entity read 
as the class in the CamelCxfRsResponseClass header when that is set. With 
httpClientAPI=false: the return value of the resource method, null for a void 
one" }
   },
   "properties": {
diff --git 
a/components/camel-cxf/camel-cxf-rest/src/main/docs/cxfrs-component.adoc 
b/components/camel-cxf/camel-cxf-rest/src/main/docs/cxfrs-component.adoc
index 94a2892c2e33..b67f7f19e031 100644
--- a/components/camel-cxf/camel-cxf-rest/src/main/docs/cxfrs-component.adoc
+++ b/components/camel-cxf/camel-cxf-rest/src/main/docs/cxfrs-component.adoc
@@ -101,7 +101,11 @@ The `Default` binding style is rather low-level, requiring 
the user to
 manually process the `MessageContentsList` object coming into the route.
 Thus, it tightly couples the route logic with the method signature and
 parameter indices of the JAX-RS operation. Somewhat inelegant, difficult
-and error-prone.
+and error-prone. When a route with the `Default` binding style does not set
+a reply body, the request `MessageContentsList` of one parameter is the
+response entity, and one without parameters replies with no content (204),
+unless the resource method returns a collection. A `MessageContentsList` of
+several parameters fails with HTTP 500, so set the reply body in the route.
 
 In contrast, the `SimpleConsumer` binding style performs the following
 mappings, to *make the request data more accessible* to you
@@ -425,6 +429,11 @@ let the producer use the http centric client API by using 
the URI option
 You can turn the response object to the type class specified with the
 message
 header `CamelCxfRsResponseClass`.
+The message body is the request entity (a GET sends none): a `List` or an
+array of primitives (such as `byte[]` or `int[]`) is sent as a whole, and of a
+`MessageContentsList` (such as the body from a `cxfrs` consumer with the
+`Default` binding style) or an `Object[]` only the first element is sent, as
+they carry the parameters of a method call.
 
 ._Java-only: Java test API (ProducerTemplate with inline Processor)_
 [source,java]
diff --git 
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/CxfRsEndpoint.java
 
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/CxfRsEndpoint.java
index aede2015b59e..a50c8e0c89e2 100644
--- 
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/CxfRsEndpoint.java
+++ 
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/CxfRsEndpoint.java
@@ -98,8 +98,10 @@ import static 
org.apache.camel.component.cxf.common.message.CxfConstants.SCHEME_
                                      description = "The response entity: a 
jakarta.ws.rs.core.Response, or text, bytes, a"
                                                    + " stream, a file, or an 
object a JAX-RS provider writes for the"
                                                    + " Content-Type (a JAXB 
bean as XML); a Map or a List fails without a"
-                                                   + " provider for it. With 
the Default binding the request"
-                                                   + " MessageContentsList 
fails: set the reply body"),
+                                                   + " provider for it. With 
the Default binding an unchanged request"
+                                                   + " MessageContentsList 
replies with its single element, or no content"
+                                                   + " when empty (unless the 
method returns a collection); one of several"
+                                                   + " parameters fails: set 
the reply body"),
            producerIn = @BodyType(value = {
                    Carrier.NULL, Carrier.TEXT, Carrier.BYTES, Carrier.STREAM, 
Carrier.FILE,
                    Carrier.POJO },
@@ -108,8 +110,10 @@ import static 
org.apache.camel.component.cxf.common.message.CxfConstants.SCHEME_
                                                                     
Carrier.LIST, Carrier.TEXT, Carrier.VALUE,
                                                                     
Carrier.POJO, Carrier.NULL }),
                                   description = "The request entity, written 
for the Content-Type header: text, bytes, a"
-                                                + " stream, a file or an 
object a JAX-RS provider writes; of a List only"
-                                                + " the first element is sent, 
and a Map fails. A GET sends no body. With"
+                                                + " stream, a file or an 
object a JAX-RS provider writes, such as a List as"
+                                                + " a whole (of a 
MessageContentsList or an Object[] only the first element"
+                                                + " is sent); a"
+                                                + " Map fails. A GET sends no 
body. With"
                                                 + " httpClientAPI=false: the 
parameters of the resource method named by the"
                                                 + " CamelCxfOperationName 
header, as a List or the one parameter"),
            producerOut = @BodyType(value = Carrier.POJO,
diff --git 
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBinding.java
 
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBinding.java
index 296d548ba070..7435e3868182 100644
--- 
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBinding.java
+++ 
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBinding.java
@@ -19,6 +19,7 @@ package org.apache.camel.component.cxf.jaxrs;
 import java.lang.reflect.Method;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
@@ -47,6 +48,7 @@ import org.apache.cxf.helpers.CastUtils;
 import org.apache.cxf.helpers.HttpHeaderHelper;
 import org.apache.cxf.jaxrs.client.WebClient;
 import org.apache.cxf.jaxrs.impl.MetadataMap;
+import org.apache.cxf.jaxrs.model.OperationResourceInfo;
 import org.apache.cxf.jaxrs.model.OperationResourceInfoStack;
 import org.apache.cxf.message.MessageContentsList;
 import org.apache.cxf.security.LoginSecurityContext;
@@ -70,6 +72,28 @@ public class DefaultCxfRsBinding implements CxfRsBinding, 
HeaderFilterStrategyAw
             Exchange camelExchange,
             org.apache.cxf.message.Exchange cxfExchange)
             throws Exception {
+        Object o = doPopulateCxfRsResponseFromExchange(camelExchange, 
cxfExchange);
+        if (o instanceof MessageContentsList mcl && mcl.size() <= 1 && 
!isCollectionReturnType(cxfExchange)) {
+            // the request parameters are still the body (the route did not 
set a reply), and CXF has no
+            // message body writer for a MessageContentsList: reply with its 
single element, or no content
+            return mcl.isEmpty() ? null : mcl.get(0);
+        }
+        return o;
+    }
+
+    private static boolean 
isCollectionReturnType(org.apache.cxf.message.Exchange cxfExchange) {
+        // a resource method that returns a collection writes the 
MessageContentsList as that collection
+        OperationResourceInfo ori = cxfExchange != null ? 
cxfExchange.get(OperationResourceInfo.class) : null;
+        return ori != null && 
Collection.class.isAssignableFrom(ori.getMethodToInvoke().getReturnType());
+    }
+
+    /**
+     * Populates the CXF exchange from the Camel response message and returns 
its body as is.
+     */
+    protected Object doPopulateCxfRsResponseFromExchange(
+            Exchange camelExchange,
+            org.apache.cxf.message.Exchange cxfExchange)
+            throws Exception {
         // Need to check if the exchange has the exception
         if (camelExchange.isFailed() && camelExchange.getException() != null) {
             throw camelExchange.getException();
@@ -209,8 +233,9 @@ public class DefaultCxfRsBinding implements CxfRsBinding, 
HeaderFilterStrategyAw
 
     /**
      * This method call Message.getBody({@link MessageContentsList}) to allow 
an appropriate converter to kick in even
-     * through we only read the first element off the MessageContextList. If 
that returns null, we check the body to see
-     * if it is a List or an array and then return the first element. If that 
fails, we will simply return the object.
+     * through we only read the first element off the MessageContextList (no 
body when it is empty). Any other List is
+     * the request entity as a whole. If the body is an array of objects, we 
return its first element; an array of
+     * primitives (such as byte[]) is the request entity as a whole. 
Otherwise, we will simply return the object.
      */
     @Override
     public Object bindCamelMessageBodyToRequestBody(Message camelMessage, 
Exchange camelExchange)
@@ -218,16 +243,13 @@ public class DefaultCxfRsBinding implements CxfRsBinding, 
HeaderFilterStrategyAw
 
         Object request = camelMessage.getBody(MessageContentsList.class);
         if (request instanceof MessageContentsList mcl) {
-            return mcl.get(0);
+            return mcl.isEmpty() ? null : mcl.get(0);
         }
 
         request = camelMessage.getBody();
-        if (request instanceof List) {
-            request = ((List<?>) request).get(0);
-        } else if (request instanceof byte[] byteArray) {
-            return byteArray;
-        } else if (request != null && request.getClass().isArray()) {
-            request = ((Object[]) request)[0];
+        if (request instanceof Object[] array && array.length > 0) {
+            // like a MessageContentsList, an array of objects carries the 
entity first
+            request = array[0];
         }
 
         return request;
diff --git 
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/SimpleCxfRsBinding.java
 
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/SimpleCxfRsBinding.java
index fd9f6c108573..5422d6a3135b 100644
--- 
a/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/SimpleCxfRsBinding.java
+++ 
b/components/camel-cxf/camel-cxf-rest/src/main/java/org/apache/camel/component/cxf/jaxrs/SimpleCxfRsBinding.java
@@ -154,7 +154,8 @@ public class SimpleCxfRsBinding extends DefaultCxfRsBinding 
{
     @Override
     public Object populateCxfRsResponseFromExchange(org.apache.camel.Exchange 
camelExchange, Exchange cxfExchange)
             throws Exception {
-        Object base = super.populateCxfRsResponseFromExchange(camelExchange, 
cxfExchange);
+        // the request MessageContentsList is not unwrapped: buildResponse 
replies without an entity for it
+        Object base = super.doPopulateCxfRsResponseFromExchange(camelExchange, 
cxfExchange);
         return buildResponse(camelExchange, base);
     }
 
diff --git 
a/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/CxfRsConsumerUnchangedBodyTest.java
 
b/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/CxfRsConsumerUnchangedBodyTest.java
new file mode 100644
index 000000000000..63d3db9085c8
--- /dev/null
+++ 
b/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/CxfRsConsumerUnchangedBodyTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.cxf.jaxrs;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.cxf.jaxrs.response.MyResponse;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.hc.client5.http.classic.methods.HttpGet;
+import org.apache.hc.client5.http.classic.methods.HttpPut;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * With the Default binding a route that does not set a reply body replies 
with the request entity instead of failing on
+ * the request MessageContentsList.
+ */
+public class CxfRsConsumerUnchangedBodyTest extends CamelTestSupport {
+    @RegisterExtension
+    static final AvailablePortFinder.Port PORT = AvailablePortFinder.find();
+
+    private static final String PUT_REQUEST = 
"<Customer><name>Mary</name><id>123</id></Customer>";
+    private static final String TEST_URL
+            = "http://localhost:"; + PORT.getPort() + 
"/CxfRsConsumerUnchangedBodyTest/customerservice/customers";
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("cxfrs://http://localhost:"; + PORT.getPort() + 
"/CxfRsConsumerUnchangedBodyTest"
+                     + 
"?resourceClasses=org.apache.camel.component.cxf.jaxrs.testbean.CustomerService")
+                        // the route does not change the body
+                        .log("${header.operationName}")
+                        .to("mock:result");
+            }
+        };
+    }
+
+    @Test
+    public void testUnchangedEntityIsTheReply() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+
+        HttpPut put = new HttpPut(TEST_URL);
+        put.setEntity(new StringEntity(PUT_REQUEST, 
ContentType.parse("text/xml; charset=ISO-8859-1")));
+        put.addHeader("Accept", "text/xml");
+
+        try (CloseableHttpClient httpclient = 
HttpClientBuilder.create().build()) {
+            MyResponse httpResponse = httpclient.execute(put,
+                    response -> new MyResponse(response.getCode(), 
EntityUtils.toString(response.getEntity())));
+            assertEquals(200, httpResponse.status(), httpResponse.toString());
+            assertTrue(httpResponse.content().contains("<name>Mary</name>"), 
httpResponse.content());
+            assertTrue(httpResponse.content().contains("<id>123</id>"), 
httpResponse.content());
+        }
+
+        mock.assertIsSatisfied();
+    }
+
+    @Test
+    public void testUnchangedEmptyParametersOfCollectionMethod() throws 
Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+
+        // getCustomers() returns a List, so the empty MessageContentsList is 
written as an empty collection
+        HttpGet get = new HttpGet(TEST_URL);
+
+        try (CloseableHttpClient httpclient = 
HttpClientBuilder.create().build()) {
+            MyResponse httpResponse = httpclient.execute(get,
+                    response -> new MyResponse(response.getCode(), 
EntityUtils.toString(response.getEntity())));
+            assertEquals(200, httpResponse.status(), httpResponse.toString());
+            
assertTrue(httpResponse.content().contains("<Customers></Customers>"), 
httpResponse.content());
+        }
+
+        mock.assertIsSatisfied();
+    }
+}
diff --git 
a/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBindingTest.java
 
b/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBindingTest.java
index 25466ddd5229..ccc8f4e8c251 100644
--- 
a/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBindingTest.java
+++ 
b/components/camel-cxf/camel-cxf-rest/src/test/java/org/apache/camel/component/cxf/jaxrs/DefaultCxfRsBindingTest.java
@@ -25,8 +25,10 @@ import java.util.Map;
 
 import jakarta.ws.rs.client.Entity;
 import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.Response;
 
 import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePattern;
 import org.apache.camel.Message;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.support.DefaultExchange;
@@ -34,11 +36,16 @@ import org.apache.camel.support.DefaultHeaderFilterStrategy;
 import org.apache.camel.support.DefaultMessage;
 import org.apache.camel.support.ExchangeHelper;
 import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.message.ExchangeImpl;
+import org.apache.cxf.message.MessageContentsList;
 import org.apache.cxf.message.MessageImpl;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
 
 public class DefaultCxfRsBindingTest {
 
@@ -104,4 +111,86 @@ public class DefaultCxfRsBindingTest {
         assertNull(camelMessage.getHeader("zeroSizeList"), "We should get 
nothing here");
     }
 
+    @Test
+    public void testResponseUnwrapsSingleElementMessageContentsList() throws 
Exception {
+        DefaultCxfRsBinding binding = new DefaultCxfRsBinding();
+
+        assertEquals("Mary", binding.populateCxfRsResponseFromExchange(
+                replyExchange(new MessageContentsList("Mary")), new 
ExchangeImpl()));
+        // no content when the resource method has no parameter
+        assertNull(binding.populateCxfRsResponseFromExchange(
+                replyExchange(new MessageContentsList()), new ExchangeImpl()));
+        // several parameters are left as is
+        MessageContentsList several = new MessageContentsList("123", "Mary");
+        assertSame(several, 
binding.populateCxfRsResponseFromExchange(replyExchange(several), new 
ExchangeImpl()));
+        // a List the route sets as reply is the entity
+        List<String> list = List.of("Mary");
+        assertSame(list, 
binding.populateCxfRsResponseFromExchange(replyExchange(list), new 
ExchangeImpl()));
+    }
+
+    @Test
+    public void testSimpleBindingResponseHasNoEntityForMessageContentsList() 
throws Exception {
+        SimpleCxfRsBinding binding = new SimpleCxfRsBinding();
+        binding.setHeaderFilterStrategy(new DefaultHeaderFilterStrategy());
+
+        Object answer = binding.populateCxfRsResponseFromExchange(
+                replyExchange(new MessageContentsList("123")), new 
ExchangeImpl());
+        Response response = assertInstanceOf(Response.class, answer);
+        assertFalse(response.hasEntity());
+    }
+
+    @Test
+    public void testRequestBodyOfList() throws Exception {
+        DefaultCxfRsBinding binding = new DefaultCxfRsBinding();
+        Exchange exchange = new DefaultExchange(context);
+
+        // all the elements of a List are sent
+        List<String> list = List.of("a", "b", "c");
+        exchange.getIn().setBody(list);
+        assertSame(list, 
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+        List<String> single = List.of("a");
+        exchange.getIn().setBody(single);
+        assertSame(single, 
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+        // the entity of a MessageContentsList is its first element
+        exchange.getIn().setBody(new MessageContentsList("a", "b"));
+        assertEquals("a", 
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+        exchange.getIn().setBody(new MessageContentsList());
+        assertNull(binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), 
exchange));
+    }
+
+    @Test
+    public void testRequestBodyOfArray() throws Exception {
+        DefaultCxfRsBinding binding = new DefaultCxfRsBinding();
+        Exchange exchange = new DefaultExchange(context);
+
+        // a primitive array is the entity as a whole
+        int[] ints = { 1, 2, 3 };
+        exchange.getIn().setBody(ints);
+        assertSame(ints, 
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+        long[] longs = { 1L, 2L };
+        exchange.getIn().setBody(longs);
+        assertSame(longs, 
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+        byte[] bytes = { 1, 2 };
+        exchange.getIn().setBody(bytes);
+        assertSame(bytes, 
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+        // the entity of an Object[] is its first element, as of a 
MessageContentsList
+        exchange.getIn().setBody(new Object[] { "a", "b" });
+        assertEquals("a", 
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+
+        exchange.getIn().setBody(new String[] { "a", "b" });
+        assertEquals("a", 
binding.bindCamelMessageBodyToRequestBody(exchange.getIn(), exchange));
+    }
+
+    private Exchange replyExchange(Object body) {
+        Exchange exchange = new DefaultExchange(context, 
ExchangePattern.InOut);
+        exchange.getMessage().setBody(body);
+        return exchange;
+    }
+
 }
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 31e006dc2fbf..e173917f8172 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1938,6 +1938,21 @@ It is now written with all its decimals, so a record 
that is unmarshalled and ma
 `Double` that is the result of a calculation is written with all its digits 
too (`0.1 + 0.2` as `0.30000000000000004`,
 before `0.3`). Set `precision` to get a fixed number of decimals, as before.
 
+=== camel-cxfrs - an unchanged request body as reply, and a List body of the 
HTTP API producer
+
+With the `Default` binding style, a route that does not set a reply body 
replied with the request
+`MessageContentsList`, which failed with HTTP 500 "No message body writer has 
been found for class
+MessageContentsList" (or was written as a one element array by a JSON provider 
that writes any object). Now a
+`MessageContentsList` reply of one element is the response entity, and an 
empty one replies with no content (204).
+When the resource method returns a collection, the `MessageContentsList` is 
still written as that collection. A
+`MessageContentsList` of several parameters (a resource method with more than 
one parameter) still fails with HTTP 500:
+set the reply body in the route.
+
+The producer with the HTTP centric client API (`httpClientAPI=true`) sent only 
the first element of a `List` body. Now
+a `List` body is sent as a whole as the request entity, also a `List` of one 
element. Of a `MessageContentsList` (such
+as the body from a `cxfrs` consumer with the `Default` binding style) the 
first element is still sent. To send one
+element as before, set that element as the body.
+
 === camel-stax - the text mode of xtokenize gives the text as an XML parser 
reads it
 
 With mode `t`, `xtokenize` returned the characters between the tags of the 
element as they are written: entity and

Reply via email to