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

davsclaus pushed a commit to branch CAMEL-26188-cometd-body
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 1556dffa8019f831a26d57ffa02f0b52f4ed80aa
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 23:55:35 2026 +0100

    CAMEL-26188: camel-cometd - publish a stream or file body as its text, and 
a null body as a JSON null
    
    The producer and the reply of an InOut consumer set the body as is as the 
Bayeux data, so a stream, a Reader
    or a file reached the clients as the text of its toString. They are now 
read as text before publishing.
    
    A null body called setData(null), which leaves out the data field: the 
clients took the message for the
    reply of a publish and never delivered it to their subscribers. The data 
field is now set to a JSON null.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/catalog/components/cometd.json    |   6 +-
 .../apache/camel/catalog/components/cometds.json   |   6 +-
 .../org/apache/camel/component/cometd/cometd.json  |   6 +-
 .../org/apache/camel/component/cometd/cometds.json |   6 +-
 .../camel/component/cometd/CometdBinding.java      |  26 ++-
 .../camel/component/cometd/CometdEndpoint.java     |  23 ++-
 .../component/cometd/CometdProducerBodyTest.java   | 201 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  10 +
 8 files changed, 263 insertions(+), 21 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cometd.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cometd.json
index 5737e8a07f07..8dfb68350a75 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cometd.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cometd.json
@@ -41,9 +41,9 @@
     "subscription": { "index": 1, "kind": "header", "displayName": "", 
"group": "common", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The subscription", "constantName": 
"org.apache.camel.component.cometd.CometdBinding#COMETD_SUBSCRIPTION_HEADER_NAME"
 }
   },
   "bodyTypes": {
-    "consumerOut": { "carriers": [ "text", "value", "map", "list", "null" ], 
"description": "The data of the Bayeux message, parsed from JSON: a String, a 
number or a boolean, a Map for an object, a List for an array, or null. From a 
cometd producer in the same JVM the body that producer sent, as is" },
-    "consumerReply": { "carriers": [ "text", "value", "map", "list" ], 
"description": "Only when the exchange is InOut (exchangePattern=InOut, or set 
in the route): the body the route ends with is sent back to the client that 
published, as JSON. Bytes become an array of numbers; a stream, a file or 
another object is sent as the text of its toString" },
-    "producerIn": { "carriers": [ "text", "value", "map", "list" ], 
"description": "The data published to the channel, sent as JSON to the 
subscribed clients: text, a number or a boolean, a Map or a List. Bytes become 
an array of numbers; a stream, a file or another object is sent as the text of 
its toString: convert it first. The headers go in the ext field of the message" 
},
+    "consumerOut": { "carriers": [ "text", "value", "map", "list", "null" ], 
"description": "The data of the Bayeux message, parsed from JSON: a String, a 
number or a boolean, a Map for an object, a List for an array, or null. From a 
cometd producer in the same JVM the body that producer sent, as is (a stream or 
a file as its text)" },
+    "consumerReply": { "carriers": [ "text", "value", "map", "list", "stream", 
"file", "null" ], "description": "Only when the exchange is InOut 
(exchangePattern=InOut, or set in the route): the body the route ends with is 
sent back to the client that published, as JSON. A stream or a file is read as 
text, and null is sent as a JSON null. Bytes become an array of numbers; 
another object is sent as the text of its toString" },
+    "producerIn": { "carriers": [ "text", "value", "map", "list", "stream", 
"file", "null" ], "description": "The data published to the channel, sent as 
JSON to the subscribed clients: text, a number or a boolean, a Map or a List. A 
stream or a file is read as text, and null is published as a JSON null. Bytes 
become an array of numbers; another object is sent as the text of its toString: 
convert it first. The headers go in the ext field of the message" },
     "producerOut": { "carriers": [ "unchanged" ] }
   },
   "properties": {
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cometds.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cometds.json
index f20d00b45f43..bb734eaa3df7 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cometds.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/cometds.json
@@ -41,9 +41,9 @@
     "subscription": { "index": 1, "kind": "header", "displayName": "", 
"group": "common", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The subscription", "constantName": 
"org.apache.camel.component.cometd.CometdBinding#COMETD_SUBSCRIPTION_HEADER_NAME"
 }
   },
   "bodyTypes": {
-    "consumerOut": { "carriers": [ "text", "value", "map", "list", "null" ], 
"description": "The data of the Bayeux message, parsed from JSON: a String, a 
number or a boolean, a Map for an object, a List for an array, or null. From a 
cometd producer in the same JVM the body that producer sent, as is" },
-    "consumerReply": { "carriers": [ "text", "value", "map", "list" ], 
"description": "Only when the exchange is InOut (exchangePattern=InOut, or set 
in the route): the body the route ends with is sent back to the client that 
published, as JSON. Bytes become an array of numbers; a stream, a file or 
another object is sent as the text of its toString" },
-    "producerIn": { "carriers": [ "text", "value", "map", "list" ], 
"description": "The data published to the channel, sent as JSON to the 
subscribed clients: text, a number or a boolean, a Map or a List. Bytes become 
an array of numbers; a stream, a file or another object is sent as the text of 
its toString: convert it first. The headers go in the ext field of the message" 
},
+    "consumerOut": { "carriers": [ "text", "value", "map", "list", "null" ], 
"description": "The data of the Bayeux message, parsed from JSON: a String, a 
number or a boolean, a Map for an object, a List for an array, or null. From a 
cometd producer in the same JVM the body that producer sent, as is (a stream or 
a file as its text)" },
+    "consumerReply": { "carriers": [ "text", "value", "map", "list", "stream", 
"file", "null" ], "description": "Only when the exchange is InOut 
(exchangePattern=InOut, or set in the route): the body the route ends with is 
sent back to the client that published, as JSON. A stream or a file is read as 
text, and null is sent as a JSON null. Bytes become an array of numbers; 
another object is sent as the text of its toString" },
+    "producerIn": { "carriers": [ "text", "value", "map", "list", "stream", 
"file", "null" ], "description": "The data published to the channel, sent as 
JSON to the subscribed clients: text, a number or a boolean, a Map or a List. A 
stream or a file is read as text, and null is published as a JSON null. Bytes 
become an array of numbers; another object is sent as the text of its toString: 
convert it first. The headers go in the ext field of the message" },
     "producerOut": { "carriers": [ "unchanged" ] }
   },
   "properties": {
diff --git 
a/components/camel-cometd/src/generated/resources/META-INF/org/apache/camel/component/cometd/cometd.json
 
b/components/camel-cometd/src/generated/resources/META-INF/org/apache/camel/component/cometd/cometd.json
index 5737e8a07f07..8dfb68350a75 100644
--- 
a/components/camel-cometd/src/generated/resources/META-INF/org/apache/camel/component/cometd/cometd.json
+++ 
b/components/camel-cometd/src/generated/resources/META-INF/org/apache/camel/component/cometd/cometd.json
@@ -41,9 +41,9 @@
     "subscription": { "index": 1, "kind": "header", "displayName": "", 
"group": "common", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The subscription", "constantName": 
"org.apache.camel.component.cometd.CometdBinding#COMETD_SUBSCRIPTION_HEADER_NAME"
 }
   },
   "bodyTypes": {
-    "consumerOut": { "carriers": [ "text", "value", "map", "list", "null" ], 
"description": "The data of the Bayeux message, parsed from JSON: a String, a 
number or a boolean, a Map for an object, a List for an array, or null. From a 
cometd producer in the same JVM the body that producer sent, as is" },
-    "consumerReply": { "carriers": [ "text", "value", "map", "list" ], 
"description": "Only when the exchange is InOut (exchangePattern=InOut, or set 
in the route): the body the route ends with is sent back to the client that 
published, as JSON. Bytes become an array of numbers; a stream, a file or 
another object is sent as the text of its toString" },
-    "producerIn": { "carriers": [ "text", "value", "map", "list" ], 
"description": "The data published to the channel, sent as JSON to the 
subscribed clients: text, a number or a boolean, a Map or a List. Bytes become 
an array of numbers; a stream, a file or another object is sent as the text of 
its toString: convert it first. The headers go in the ext field of the message" 
},
+    "consumerOut": { "carriers": [ "text", "value", "map", "list", "null" ], 
"description": "The data of the Bayeux message, parsed from JSON: a String, a 
number or a boolean, a Map for an object, a List for an array, or null. From a 
cometd producer in the same JVM the body that producer sent, as is (a stream or 
a file as its text)" },
+    "consumerReply": { "carriers": [ "text", "value", "map", "list", "stream", 
"file", "null" ], "description": "Only when the exchange is InOut 
(exchangePattern=InOut, or set in the route): the body the route ends with is 
sent back to the client that published, as JSON. A stream or a file is read as 
text, and null is sent as a JSON null. Bytes become an array of numbers; 
another object is sent as the text of its toString" },
+    "producerIn": { "carriers": [ "text", "value", "map", "list", "stream", 
"file", "null" ], "description": "The data published to the channel, sent as 
JSON to the subscribed clients: text, a number or a boolean, a Map or a List. A 
stream or a file is read as text, and null is published as a JSON null. Bytes 
become an array of numbers; another object is sent as the text of its toString: 
convert it first. The headers go in the ext field of the message" },
     "producerOut": { "carriers": [ "unchanged" ] }
   },
   "properties": {
diff --git 
a/components/camel-cometd/src/generated/resources/META-INF/org/apache/camel/component/cometd/cometds.json
 
b/components/camel-cometd/src/generated/resources/META-INF/org/apache/camel/component/cometd/cometds.json
index f20d00b45f43..bb734eaa3df7 100644
--- 
a/components/camel-cometd/src/generated/resources/META-INF/org/apache/camel/component/cometd/cometds.json
+++ 
b/components/camel-cometd/src/generated/resources/META-INF/org/apache/camel/component/cometd/cometds.json
@@ -41,9 +41,9 @@
     "subscription": { "index": 1, "kind": "header", "displayName": "", 
"group": "common", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The subscription", "constantName": 
"org.apache.camel.component.cometd.CometdBinding#COMETD_SUBSCRIPTION_HEADER_NAME"
 }
   },
   "bodyTypes": {
-    "consumerOut": { "carriers": [ "text", "value", "map", "list", "null" ], 
"description": "The data of the Bayeux message, parsed from JSON: a String, a 
number or a boolean, a Map for an object, a List for an array, or null. From a 
cometd producer in the same JVM the body that producer sent, as is" },
-    "consumerReply": { "carriers": [ "text", "value", "map", "list" ], 
"description": "Only when the exchange is InOut (exchangePattern=InOut, or set 
in the route): the body the route ends with is sent back to the client that 
published, as JSON. Bytes become an array of numbers; a stream, a file or 
another object is sent as the text of its toString" },
-    "producerIn": { "carriers": [ "text", "value", "map", "list" ], 
"description": "The data published to the channel, sent as JSON to the 
subscribed clients: text, a number or a boolean, a Map or a List. Bytes become 
an array of numbers; a stream, a file or another object is sent as the text of 
its toString: convert it first. The headers go in the ext field of the message" 
},
+    "consumerOut": { "carriers": [ "text", "value", "map", "list", "null" ], 
"description": "The data of the Bayeux message, parsed from JSON: a String, a 
number or a boolean, a Map for an object, a List for an array, or null. From a 
cometd producer in the same JVM the body that producer sent, as is (a stream or 
a file as its text)" },
+    "consumerReply": { "carriers": [ "text", "value", "map", "list", "stream", 
"file", "null" ], "description": "Only when the exchange is InOut 
(exchangePattern=InOut, or set in the route): the body the route ends with is 
sent back to the client that published, as JSON. A stream or a file is read as 
text, and null is sent as a JSON null. Bytes become an array of numbers; 
another object is sent as the text of its toString" },
+    "producerIn": { "carriers": [ "text", "value", "map", "list", "stream", 
"file", "null" ], "description": "The data published to the channel, sent as 
JSON to the subscribed clients: text, a number or a boolean, a Map or a List. A 
stream or a file is read as text, and null is published as a JSON null. Bytes 
become an array of numbers; another object is sent as the text of its toString: 
convert it first. The headers go in the ext field of the message" },
     "producerOut": { "carriers": [ "unchanged" ] }
   },
   "properties": {
diff --git 
a/components/camel-cometd/src/main/java/org/apache/camel/component/cometd/CometdBinding.java
 
b/components/camel-cometd/src/main/java/org/apache/camel/component/cometd/CometdBinding.java
index 814954d26253..4313aad98a77 100644
--- 
a/components/camel-cometd/src/main/java/org/apache/camel/component/cometd/CometdBinding.java
+++ 
b/components/camel-cometd/src/main/java/org/apache/camel/component/cometd/CometdBinding.java
@@ -16,6 +16,10 @@
  */
 package org.apache.camel.component.cometd;
 
+import java.io.File;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.file.Path;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -24,6 +28,8 @@ import java.util.Set;
 import org.apache.camel.CamelContext;
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
+import org.apache.camel.StreamCache;
+import org.apache.camel.WrappedFile;
 import org.apache.camel.spi.HeaderFilterStrategy;
 import org.apache.camel.spi.Metadata;
 import org.apache.camel.support.DefaultHeaderFilterStrategy;
@@ -35,6 +41,8 @@ import org.cometd.server.BayeuxServerImpl;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import static org.cometd.bayeux.Message.DATA_FIELD;
+
 /**
  * A Strategy used to convert between a Camel {@link Exchange} and to and from 
a Cometd messages
  */
@@ -75,10 +83,26 @@ public class CometdBinding {
             mutable.setClientId(serverSession.getId());
         }
         addHeadersToMessage(mutable, camelMessage);
-        mutable.setData(camelMessage.getBody());
+        Object body = camelMessage.getBody();
+        if (body == null) {
+            // publish a JSON null: setData(null) leaves out the data field, 
and the clients take a message without
+            // data for the reply of a publish, so their subscribers never 
receive it
+            mutable.put(DATA_FIELD, null);
+        } else {
+            if (isStreamOrFile(body)) {
+                // the JSON of the message has the toString of a stream or a 
file, so publish its content as text
+                body = camelMessage.getBody(String.class);
+            }
+            mutable.setData(body);
+        }
         return mutable;
     }
 
+    private static boolean isStreamOrFile(Object body) {
+        return body instanceof StreamCache || body instanceof InputStream || 
body instanceof Reader
+                || body instanceof File || body instanceof Path || body 
instanceof WrappedFile;
+    }
+
     public Message createCamelMessage(
             CamelContext camelContext, ServerSession remote, ServerMessage 
cometdMessage, Object data) {
         if (cometdMessage != null) {
diff --git 
a/components/camel-cometd/src/main/java/org/apache/camel/component/cometd/CometdEndpoint.java
 
b/components/camel-cometd/src/main/java/org/apache/camel/component/cometd/CometdEndpoint.java
index e0a3c215dfbc..2c846c0b2d78 100644
--- 
a/components/camel-cometd/src/main/java/org/apache/camel/component/cometd/CometdEndpoint.java
+++ 
b/components/camel-cometd/src/main/java/org/apache/camel/component/cometd/CometdEndpoint.java
@@ -46,17 +46,24 @@ import org.apache.camel.util.ObjectHelper;
 @BodyTypes(consumerOut = @BodyType(value = { Carrier.TEXT, Carrier.VALUE, 
Carrier.MAP, Carrier.LIST, Carrier.NULL },
                                    description = "The data of the Bayeux 
message, parsed from JSON: a String, a number or a"
                                                  + " boolean, a Map for an 
object, a List for an array, or null. From a cometd"
-                                                 + " producer in the same JVM 
the body that producer sent, as is"),
-           consumerReply = @BodyType(value = { Carrier.TEXT, Carrier.VALUE, 
Carrier.MAP, Carrier.LIST },
+                                                 + " producer in the same JVM 
the body that producer sent, as is (a stream or"
+                                                 + " a file as its text)"),
+           consumerReply = @BodyType(value = {
+                   Carrier.TEXT, Carrier.VALUE, Carrier.MAP, Carrier.LIST, 
Carrier.STREAM,
+                   Carrier.FILE, Carrier.NULL },
                                      description = "Only when the exchange is 
InOut (exchangePattern=InOut, or set in the"
                                                    + " route): the body the 
route ends with is sent back to the client that"
-                                                   + " published, as JSON. 
Bytes become an array of numbers; a stream, a file"
-                                                   + " or another object is 
sent as the text of its toString"),
-           producerIn = @BodyType(value = { Carrier.TEXT, Carrier.VALUE, 
Carrier.MAP, Carrier.LIST },
+                                                   + " published, as JSON. A 
stream or a file is read as text, and null is"
+                                                   + " sent as a JSON null. 
Bytes become an array of numbers; another object"
+                                                   + " is sent as the text of 
its toString"),
+           producerIn = @BodyType(value = {
+                   Carrier.TEXT, Carrier.VALUE, Carrier.MAP, Carrier.LIST, 
Carrier.STREAM,
+                   Carrier.FILE, Carrier.NULL },
                                   description = "The data published to the 
channel, sent as JSON to the subscribed clients:"
-                                                + " text, a number or a 
boolean, a Map or a List. Bytes become an array of"
-                                                + " numbers; a stream, a file 
or another object is sent as the text of its"
-                                                + " toString: convert it 
first. The headers go in the ext field of the message"),
+                                                + " text, a number or a 
boolean, a Map or a List. A stream or a file is read"
+                                                + " as text, and null is 
published as a JSON null. Bytes become an array of"
+                                                + " numbers; another object is 
sent as the text of its toString: convert it"
+                                                + " first. The headers go in 
the ext field of the message"),
            producerOut = @BodyType(Carrier.UNCHANGED))
 public class CometdEndpoint extends DefaultEndpoint implements 
EndpointServiceLocation {
 
diff --git 
a/components/camel-cometd/src/test/java/org/apache/camel/component/cometd/CometdProducerBodyTest.java
 
b/components/camel-cometd/src/test/java/org/apache/camel/component/cometd/CometdProducerBodyTest.java
new file mode 100644
index 000000000000..2140027a0aa6
--- /dev/null
+++ 
b/components/camel-cometd/src/test/java/org/apache/camel/component/cometd/CometdProducerBodyTest.java
@@ -0,0 +1,201 @@
+/*
+ * 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.cometd;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.ExchangePattern;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.converter.stream.ByteArrayInputStreamCache;
+import org.apache.camel.test.AvailablePortFinder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.cometd.bayeux.Message;
+import org.cometd.client.BayeuxClient;
+import org.cometd.client.http.jetty.JettyHttpClientTransport;
+import org.eclipse.jetty.client.HttpClient;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The body a cometd producer publishes, and the body a cometd consumer 
replies with, as a BayeuxClient receives them.
+ */
+public class CometdProducerBodyTest extends CamelTestSupport {
+
+    private static final String CHANNEL = "/test/body";
+    private static final String SERVICE_CHANNEL = "/service/reply";
+
+    @RegisterExtension
+    AvailablePortFinder.Port port = AvailablePortFinder.find();
+
+    @TempDir
+    Path tempDir;
+
+    private final BlockingQueue<Message> received = new 
LinkedBlockingQueue<>();
+    private HttpClient httpClient;
+    private BayeuxClient client;
+
+    @BeforeEach
+    void connectClient() throws Exception {
+        httpClient = new HttpClient();
+        httpClient.start();
+        client = new BayeuxClient(
+                "http://127.0.0.1:"; + port.getPort() + "/cometd", new 
JettyHttpClientTransport(null, httpClient));
+        client.handshake();
+        assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));
+
+        CountDownLatch subscribed = new CountDownLatch(2);
+        client.getChannel(CHANNEL).subscribe((channel, message) -> 
received.add(message),
+                message -> subscribed.countDown());
+        client.getChannel(SERVICE_CHANNEL).subscribe((channel, message) -> 
received.add(message),
+                message -> subscribed.countDown());
+        assertTrue(subscribed.await(5, TimeUnit.SECONDS), "Should subscribe 
within timeout");
+    }
+
+    @AfterEach
+    void disconnectClient() throws Exception {
+        client.disconnect();
+        client.waitFor(5000, BayeuxClient.State.DISCONNECTED);
+        httpClient.stop();
+    }
+
+    @Test
+    void testTextBody() throws Exception {
+        template.sendBody("direct:input", "Hello World");
+
+        assertEquals("Hello World", receive().getData());
+    }
+
+    @Test
+    void testInputStreamBody() throws Exception {
+        template.sendBody("direct:input", new ByteArrayInputStream("Hello 
Stream".getBytes(StandardCharsets.UTF_8)));
+
+        assertEquals("Hello Stream", receive().getData());
+    }
+
+    @Test
+    void testStreamCacheBody() throws Exception {
+        template.sendBody("direct:input", new ByteArrayInputStreamCache(
+                new ByteArrayInputStream("Hello Stream 
Cache".getBytes(StandardCharsets.UTF_8))));
+
+        assertEquals("Hello Stream Cache", receive().getData());
+    }
+
+    @Test
+    void testReaderBody() throws Exception {
+        template.sendBody("direct:input", new StringReader("Hello Reader"));
+
+        assertEquals("Hello Reader", receive().getData());
+    }
+
+    @Test
+    void testFileBody() throws Exception {
+        Path file = Files.writeString(tempDir.resolve("hello.txt"), "Hello 
File");
+
+        template.sendBody("direct:input", file.toFile());
+        assertEquals("Hello File", receive().getData());
+
+        template.sendBody("direct:input", file);
+        assertEquals("Hello File", receive().getData());
+    }
+
+    @Test
+    void testFileConsumerBody() throws Exception {
+        Files.writeString(tempDir.resolve("hello.txt"), "Hello File Consumer");
+
+        context.getRouteController().startRoute("file");
+
+        assertEquals("Hello File Consumer", receive().getData());
+    }
+
+    @Test
+    void testNullBody() throws Exception {
+        template.sendBody("direct:input", null);
+
+        Message message = receive();
+        assertEquals(CHANNEL, message.getChannel());
+        assertTrue(message.containsKey(Message.DATA_FIELD));
+        assertNull(message.getData());
+    }
+
+    @Test
+    void testNullReply() throws Exception {
+        client.getChannel(SERVICE_CHANNEL).publish("Hello");
+
+        Message message = receive();
+        assertEquals(SERVICE_CHANNEL, message.getChannel());
+        assertTrue(message.containsKey(Message.DATA_FIELD));
+        assertNull(message.getData());
+    }
+
+    @Test
+    void testStreamReply() throws Exception {
+        client.getChannel(SERVICE_CHANNEL).publish("stream");
+
+        assertEquals("Reply stream", receive().getData());
+    }
+
+    private Message receive() throws InterruptedException {
+        Message message = received.poll(5, TimeUnit.SECONDS);
+        assertNotNull(message, "Should receive a message within timeout");
+        return message;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        String options = 
"?baseResource=file:./target/test-classes/webapp&timeout=240000&interval=0&maxInterval=30000"
+                         + 
"&multiFrameInterval=1500&jsonCommented=true&logLevel=2";
+        String producer = "cometd://127.0.0.1:" + port.getPort() + CHANNEL + 
options;
+        String consumer = "cometd://127.0.0.1:" + port.getPort() + 
SERVICE_CHANNEL + options;
+
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:input").to(producer);
+
+                File dir = tempDir.toFile();
+                fromF("file:%s?noop=true&initialDelay=0&delay=10", 
dir.getAbsolutePath()).routeId("file").autoStartup(false)
+                        .to(producer);
+
+                from(consumer)
+                        .setExchangePattern(ExchangePattern.InOut)
+                        .choice()
+                        .when(body().isEqualTo("stream"))
+                        .process(exchange -> exchange.getMessage().setBody(
+                                new ByteArrayInputStream("Reply 
stream".getBytes(StandardCharsets.UTF_8))))
+                        .otherwise()
+                        .process(exchange -> 
exchange.getMessage().setBody(null));
+            }
+        };
+    }
+}
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 10f5a47c1b89..b3dfa7b63373 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
@@ -711,6 +711,16 @@ with `defaultFrom`.
 When the validation of an `SMSMessage` fails, the 
`InvalidPayloadRuntimeException` now names the invalid fields in its
 message, and its cause is an `org.apache.camel.ValidationException`.
 
+=== camel-cometd - stream, file and null bodies are published
+
+The cometd producer, and the reply of an InOut cometd consumer, now publish a 
stream, a `Reader` or a file body
+(`File`, `Path` or the body of the file consumer) as its content as text. 
Before, the clients received the text of its
+`toString`, such as `ByteArrayInputStreamCache@...` or the path of the file. A 
cometd consumer in the same JVM now
+receives such a body as a `String`.
+
+A `null` body is now published as a JSON `null` that the subscribed clients 
receive. Before, the message had no data,
+so the clients took it for the reply of a publish and never received it, while 
the exchange succeeded.
+
 === camel-aws - the HTTP client is no longer looked up on the classpath
 
 The camel-aws components now set the HTTP client of the AWS SDK clients they 
create for their endpoints: the Apache 5

Reply via email to