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 020181ed8a63 CAMEL-26158: camel-vertx - reply with a stream as a 
Buffer, and with a failure when the reply has no codec (#28941)
020181ed8a63 is described below

commit 020181ed8a632187d2f0dab51a150c239e600aba
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:50:54 2026 +0100

    CAMEL-26158: camel-vertx - reply with a stream as a Buffer, and with a 
failure when the reply has no codec (#28941)
    
    * CAMEL-26158: camel-vertx - reply with a stream as a Buffer, and with …
    
    …a failure when the reply has no codec
    
    The consumer called event.reply(body) and nobody caught the 
IllegalArgumentException that
    Vert.x throws when the body has no codec, so no reply was sent and the 
requester waited until
    its timeout. An echo route of a Buffer request hit this too, as stream 
caching turns the Buffer
    into a stream cache. The consumer now replies with a stream body as a 
Buffer, and when the reply
    still cannot be sent it sends a failure reply and passes the error to the 
exception handler.
    
    The producer failed a message without a body with "No body available of 
type: java.lang.String",
    though it sends any body type; the error now says that it cannot send a 
message without a body.
    
    The pubSub option only applies to InOnly exchanges; its description and the 
docs now say so.
    
    * CAMEL-26158: camel-vertx - the producer sends a stream body as a Buffer
    
    With stream caching on, a Buffer that a vertx consumer receives becomes a 
stream cache, so a
    route from one vertx endpoint to another failed in the producer with "No 
message codec for type".
    The producer now sends a StreamCache or InputStream body as a Buffer, as 
the consumer does for
    its reply; the conversion is shared in VertxBodyHelper. Other bodies are 
sent as is.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/components/vertx.json |  6 +-
 .../apache/camel/catalog/docs/vertx-component.adoc | 18 +++--
 .../org/apache/camel/component/vertx/vertx.json    |  6 +-
 .../camel-vertx/src/main/docs/vertx-component.adoc | 18 +++--
 .../camel/component/vertx/VertxBodyHelper.java     | 47 +++++++++++
 .../camel/component/vertx/VertxConsumer.java       | 12 ++-
 .../camel/component/vertx/VertxEndpoint.java       | 16 ++--
 .../camel/component/vertx/VertxProducer.java       |  7 +-
 .../vertx/VertxRequestReplyFailureTest.java        | 31 +++++++-
 .../vertx/VertxRequestReplyStreamTest.java         | 90 ++++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    | 12 +++
 .../endpoint/dsl/VertxEndpointBuilderFactory.java  | 24 ++++--
 12 files changed, 254 insertions(+), 33 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/vertx.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/vertx.json
index 33549d87d679..68330442fedd 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/vertx.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/vertx.json
@@ -36,13 +36,13 @@
   },
   "bodyTypes": {
     "consumerOut": { "carriers": [ "unknown" ], "description": "The body of 
the event bus message, as the sender sent it: a String, a byte array, a number 
or a boolean, a JsonObject or a JsonArray, or a Serializable object such as a 
HashMap. A Buffer becomes a stream with stream caching enabled" },
-    "consumerReply": { "carriers": [ "text", "bytes", "value", "map", "list", 
"pojo", "null" ], "description": "A message with a reply address is answered 
with the body the route ends with, as the producer sends it. A stream, such as 
a received Buffer the route did not change, or a POJO that is not Serializable 
sends no reply, and the requester times out" },
-    "producerIn": { "carriers": [ "text", "bytes", "value", "map", "list", 
"pojo" ], "description": "Sent as is, with the event bus codec of its type: a 
String, a byte array, a number or a boolean, a JsonObject, a JsonArray or a 
Buffer, or a Serializable object such as a HashMap. A stream, a POJO that is 
not Serializable (without a codec registered for it) or no body fails: convert 
it first" },
+    "consumerReply": { "carriers": [ "text", "bytes", "stream", "value", 
"map", "list", "pojo", "null" ], "description": "A message with a reply address 
is answered with the body the route ends with, as the producer sends it. A 
stream, such as a received Buffer the route did not change, is replied as a 
Buffer. A POJO that is not Serializable (without a codec registered for it) is 
answered with a failure" },
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "value", "map", 
"list", "pojo" ], "description": "Sent as is, with the event bus codec of its 
type: a String, a byte array, a number or a boolean, a JsonObject, a JsonArray 
or a Buffer, or a Serializable object such as a HashMap. A stream, such as a 
received Buffer the route did not change, is sent as a Buffer. A POJO that is 
not Serializable (without a codec registered for it) or no body fails: convert 
it first" },
     "producerOut": { "carriers": [ "unknown" ], "byOption": [ { "option": 
"exchangePattern", "value": "InOnly", "carriers": [ "unchanged" ] } ], 
"description": "Unchanged for a message that expects no reply (InOnly). A 
message that expects one (InOut) is sent as a request, also with pubSub, and 
gets the body of the reply" }
   },
   "properties": {
     "address": { "index": 0, "kind": "path", "displayName": "Address", 
"group": "common", "label": "", "required": true, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Sets the event bus address used to 
communicate" },
-    "pubSub": { "index": 1, "kind": "parameter", "displayName": "Pub Sub", 
"group": "common", "label": "", "required": false, "type": "boolean", 
"javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "description": "Whether to use 
publish\/subscribe instead of point to point when sending to a vertx endpoint." 
},
+    "pubSub": { "index": 1, "kind": "parameter", "displayName": "Pub Sub", 
"group": "common", "label": "", "required": false, "type": "boolean", 
"javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "description": "Whether to use 
publish\/subscribe instead of point to point when sending to a vertx endpoint. 
This only applies when the exchange pattern is InOnly: an InOut exchange is 
sent as a request to one consumer, which replies." },
     "bridgeErrorHandler": { "index": 2, "kind": "parameter", "displayName": 
"Bridge Error Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Allows for bridging the consumer to the 
Camel routing Error Handler, which mean any exceptions (if possible) occurred 
while the Camel consumer is trying to pickup incoming  [...]
     "exceptionHandler": { "index": 3, "kind": "parameter", "displayName": 
"Exception Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "object", "javaType": 
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
let the consumer use a custom ExceptionHandler. Notice if the option 
bridgeErrorHandler is enabled then this option is not in use. By def [...]
     "exchangePattern": { "index": 4, "kind": "parameter", "displayName": 
"Exchange Pattern", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "enum", "javaType": 
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], 
"deprecated": false, "autowired": false, "secret": false, "description": "Sets 
the exchange pattern when the consumer creates an exchange." },
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/vertx-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/vertx-component.adoc
index d097b8eb6a6b..930844f5e553 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/vertx-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/vertx-component.adoc
@@ -74,9 +74,14 @@ A message goes to one consumer, or to all consumers with 
`pubSub`:
             message: "Received order ${body}"
 ----
 
-The body is sent as is, without type conversion: a stream, or a POJO that is 
not `Serializable` and has no codec
-registered on the event bus, fails with `No message codec for type`, so 
convert it to a `String` or a byte array
-first. A message without a body fails as well.
+The `pubSub` option only applies to InOnly exchanges: an InOut exchange is 
always sent as a request to one consumer,
+which replies.
+
+The body is sent as is, without type conversion, except for a stream, which is 
sent as a `Buffer`: a `Buffer` the
+consumer receives is a stream when stream caching is enabled (the default), so 
a route from one `vertx` endpoint to
+another sends it on as a `Buffer`. A POJO that is not `Serializable` and has 
no codec registered on the event bus
+fails with `No message codec for type`, so convert it to a `String` or a byte 
array first. A message without a body
+fails with an `InvalidPayloadRuntimeException`.
 
 When the exchange is InOut, the producer sends a request and the reply of the 
consumer becomes the message body:
 
@@ -101,8 +106,11 @@ When the exchange is InOut, the producer sends a request 
and the reply of the co
                 expression: "Hello ${body}"
 ----
 
-The consumer replies with the body the route ends with, which needs a codec 
too. A `Buffer` the consumer receives is
-a stream when stream caching is enabled (the default), so convert a stream 
body before the route ends.
+The consumer replies with the body the route ends with. A stream body is 
replied as a `Buffer`: a `Buffer` the
+consumer receives is a stream when stream caching is enabled (the default), so 
a route that does not change the body
+replies with a `Buffer` again. Any other body needs a codec too: when the 
reply cannot be sent, such as for a POJO
+that is not `Serializable`, the consumer logs the error and the sender gets a 
failure reply (a `ReplyException` with
+failure code 500) instead of waiting until its reply timeout.
 
 == Connecting to the existing Vert.x instance
 
diff --git 
a/components/camel-vertx/camel-vertx/src/generated/resources/META-INF/org/apache/camel/component/vertx/vertx.json
 
b/components/camel-vertx/camel-vertx/src/generated/resources/META-INF/org/apache/camel/component/vertx/vertx.json
index 33549d87d679..68330442fedd 100644
--- 
a/components/camel-vertx/camel-vertx/src/generated/resources/META-INF/org/apache/camel/component/vertx/vertx.json
+++ 
b/components/camel-vertx/camel-vertx/src/generated/resources/META-INF/org/apache/camel/component/vertx/vertx.json
@@ -36,13 +36,13 @@
   },
   "bodyTypes": {
     "consumerOut": { "carriers": [ "unknown" ], "description": "The body of 
the event bus message, as the sender sent it: a String, a byte array, a number 
or a boolean, a JsonObject or a JsonArray, or a Serializable object such as a 
HashMap. A Buffer becomes a stream with stream caching enabled" },
-    "consumerReply": { "carriers": [ "text", "bytes", "value", "map", "list", 
"pojo", "null" ], "description": "A message with a reply address is answered 
with the body the route ends with, as the producer sends it. A stream, such as 
a received Buffer the route did not change, or a POJO that is not Serializable 
sends no reply, and the requester times out" },
-    "producerIn": { "carriers": [ "text", "bytes", "value", "map", "list", 
"pojo" ], "description": "Sent as is, with the event bus codec of its type: a 
String, a byte array, a number or a boolean, a JsonObject, a JsonArray or a 
Buffer, or a Serializable object such as a HashMap. A stream, a POJO that is 
not Serializable (without a codec registered for it) or no body fails: convert 
it first" },
+    "consumerReply": { "carriers": [ "text", "bytes", "stream", "value", 
"map", "list", "pojo", "null" ], "description": "A message with a reply address 
is answered with the body the route ends with, as the producer sends it. A 
stream, such as a received Buffer the route did not change, is replied as a 
Buffer. A POJO that is not Serializable (without a codec registered for it) is 
answered with a failure" },
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "value", "map", 
"list", "pojo" ], "description": "Sent as is, with the event bus codec of its 
type: a String, a byte array, a number or a boolean, a JsonObject, a JsonArray 
or a Buffer, or a Serializable object such as a HashMap. A stream, such as a 
received Buffer the route did not change, is sent as a Buffer. A POJO that is 
not Serializable (without a codec registered for it) or no body fails: convert 
it first" },
     "producerOut": { "carriers": [ "unknown" ], "byOption": [ { "option": 
"exchangePattern", "value": "InOnly", "carriers": [ "unchanged" ] } ], 
"description": "Unchanged for a message that expects no reply (InOnly). A 
message that expects one (InOut) is sent as a request, also with pubSub, and 
gets the body of the reply" }
   },
   "properties": {
     "address": { "index": 0, "kind": "path", "displayName": "Address", 
"group": "common", "label": "", "required": true, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Sets the event bus address used to 
communicate" },
-    "pubSub": { "index": 1, "kind": "parameter", "displayName": "Pub Sub", 
"group": "common", "label": "", "required": false, "type": "boolean", 
"javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "description": "Whether to use 
publish\/subscribe instead of point to point when sending to a vertx endpoint." 
},
+    "pubSub": { "index": 1, "kind": "parameter", "displayName": "Pub Sub", 
"group": "common", "label": "", "required": false, "type": "boolean", 
"javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "description": "Whether to use 
publish\/subscribe instead of point to point when sending to a vertx endpoint. 
This only applies when the exchange pattern is InOnly: an InOut exchange is 
sent as a request to one consumer, which replies." },
     "bridgeErrorHandler": { "index": 2, "kind": "parameter", "displayName": 
"Bridge Error Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Allows for bridging the consumer to the 
Camel routing Error Handler, which mean any exceptions (if possible) occurred 
while the Camel consumer is trying to pickup incoming  [...]
     "exceptionHandler": { "index": 3, "kind": "parameter", "displayName": 
"Exception Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "object", "javaType": 
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
let the consumer use a custom ExceptionHandler. Notice if the option 
bridgeErrorHandler is enabled then this option is not in use. By def [...]
     "exchangePattern": { "index": 4, "kind": "parameter", "displayName": 
"Exchange Pattern", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "enum", "javaType": 
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], 
"deprecated": false, "autowired": false, "secret": false, "description": "Sets 
the exchange pattern when the consumer creates an exchange." },
diff --git 
a/components/camel-vertx/camel-vertx/src/main/docs/vertx-component.adoc 
b/components/camel-vertx/camel-vertx/src/main/docs/vertx-component.adoc
index d097b8eb6a6b..930844f5e553 100644
--- a/components/camel-vertx/camel-vertx/src/main/docs/vertx-component.adoc
+++ b/components/camel-vertx/camel-vertx/src/main/docs/vertx-component.adoc
@@ -74,9 +74,14 @@ A message goes to one consumer, or to all consumers with 
`pubSub`:
             message: "Received order ${body}"
 ----
 
-The body is sent as is, without type conversion: a stream, or a POJO that is 
not `Serializable` and has no codec
-registered on the event bus, fails with `No message codec for type`, so 
convert it to a `String` or a byte array
-first. A message without a body fails as well.
+The `pubSub` option only applies to InOnly exchanges: an InOut exchange is 
always sent as a request to one consumer,
+which replies.
+
+The body is sent as is, without type conversion, except for a stream, which is 
sent as a `Buffer`: a `Buffer` the
+consumer receives is a stream when stream caching is enabled (the default), so 
a route from one `vertx` endpoint to
+another sends it on as a `Buffer`. A POJO that is not `Serializable` and has 
no codec registered on the event bus
+fails with `No message codec for type`, so convert it to a `String` or a byte 
array first. A message without a body
+fails with an `InvalidPayloadRuntimeException`.
 
 When the exchange is InOut, the producer sends a request and the reply of the 
consumer becomes the message body:
 
@@ -101,8 +106,11 @@ When the exchange is InOut, the producer sends a request 
and the reply of the co
                 expression: "Hello ${body}"
 ----
 
-The consumer replies with the body the route ends with, which needs a codec 
too. A `Buffer` the consumer receives is
-a stream when stream caching is enabled (the default), so convert a stream 
body before the route ends.
+The consumer replies with the body the route ends with. A stream body is 
replied as a `Buffer`: a `Buffer` the
+consumer receives is a stream when stream caching is enabled (the default), so 
a route that does not change the body
+replies with a `Buffer` again. Any other body needs a codec too: when the 
reply cannot be sent, such as for a POJO
+that is not `Serializable`, the consumer logs the error and the sender gets a 
failure reply (a `ReplyException` with
+failure code 500) instead of waiting until its reply timeout.
 
 == Connecting to the existing Vert.x instance
 
diff --git 
a/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxBodyHelper.java
 
b/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxBodyHelper.java
new file mode 100644
index 000000000000..bb4b723256b1
--- /dev/null
+++ 
b/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxBodyHelper.java
@@ -0,0 +1,47 @@
+/*
+ * 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.vertx;
+
+import java.io.InputStream;
+
+import io.vertx.core.buffer.Buffer;
+import org.apache.camel.Exchange;
+import org.apache.camel.StreamCache;
+import org.apache.camel.support.MessageHelper;
+
+final class VertxBodyHelper {
+
+    private VertxBodyHelper() {
+    }
+
+    /**
+     * The body to send on the event bus. A stream has no codec on the event 
bus, so it is sent as a {@link Buffer}: a
+     * received {@link Buffer} is a stream when stream caching is enabled, and 
a route that does not change it sends it
+     * on. Any other body is sent as is.
+     */
+    static Object eventBusBody(Exchange exchange) {
+        Object body = exchange.getMessage().getBody();
+        if (body instanceof StreamCache || body instanceof InputStream) {
+            MessageHelper.resetStreamCache(exchange.getMessage());
+            byte[] bytes = 
exchange.getContext().getTypeConverter().convertTo(byte[].class, exchange, 
body);
+            if (bytes != null) {
+                return Buffer.buffer(bytes);
+            }
+        }
+        return body;
+    }
+}
diff --git 
a/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxConsumer.java
 
b/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxConsumer.java
index d66e9429193e..bac3f2ac5012 100644
--- 
a/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxConsumer.java
+++ 
b/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxConsumer.java
@@ -72,9 +72,17 @@ public class VertxConsumer extends DefaultConsumer {
                             event.fail(FAILURE_CODE, FAILURE_MESSAGE);
                         } else {
                             // always reply (also with no body) so the sender 
does not wait until its timeout
-                            Object body = exchange.getMessage().getBody();
+                            Object body = 
VertxBodyHelper.eventBusBody(exchange);
                             LOG.debug("Sending reply to: {} with body: {}", 
event.replyAddress(), body);
-                            event.reply(body);
+                            try {
+                                event.reply(body);
+                            } catch (Exception e) {
+                                // the body has no codec on the event bus, 
such as a POJO that is not Serializable,
+                                // so let the sender know that the request 
failed instead of waiting until its timeout
+                                event.fail(FAILURE_CODE, FAILURE_MESSAGE);
+                                getExceptionHandler().handleException(
+                                        "Error sending reply to: " + 
event.replyAddress(), exchange, e);
+                            }
                         }
                     }
                 }
diff --git 
a/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxEndpoint.java
 
b/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxEndpoint.java
index 2ccba990ee1c..7f2dd3161ae9 100644
--- 
a/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxEndpoint.java
+++ 
b/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxEndpoint.java
@@ -45,16 +45,19 @@ import org.apache.camel.support.DefaultEndpoint;
                                                  + " Serializable object such 
as a HashMap. A Buffer becomes a stream with"
                                                  + " stream caching enabled"),
            consumerReply = @BodyType(value = {
-                   Carrier.TEXT, Carrier.BYTES, Carrier.VALUE, Carrier.MAP, 
Carrier.LIST, Carrier.POJO, Carrier.NULL },
+                   Carrier.TEXT, Carrier.BYTES, Carrier.STREAM, Carrier.VALUE, 
Carrier.MAP, Carrier.LIST, Carrier.POJO,
+                   Carrier.NULL },
                                      description = "A message with a reply 
address is answered with the body the route ends"
                                                    + " with, as the producer 
sends it. A stream, such as a received Buffer the"
-                                                   + " route did not change, 
or a POJO that is not Serializable sends no reply,"
-                                                   + " and the requester times 
out"),
+                                                   + " route did not change, 
is replied as a Buffer. A POJO that is not"
+                                                   + " Serializable (without a 
codec registered for it) is answered with a"
+                                                   + " failure"),
            producerIn = @BodyType(value = {
-                   Carrier.TEXT, Carrier.BYTES, Carrier.VALUE, Carrier.MAP, 
Carrier.LIST, Carrier.POJO },
+                   Carrier.TEXT, Carrier.BYTES, Carrier.STREAM, Carrier.VALUE, 
Carrier.MAP, Carrier.LIST, Carrier.POJO },
                                   description = "Sent as is, with the event 
bus codec of its type: a String, a byte array, a"
                                                 + " number or a boolean, a 
JsonObject, a JsonArray or a Buffer, or a"
-                                                + " Serializable object such 
as a HashMap. A stream, a POJO that is not"
+                                                + " Serializable object such 
as a HashMap. A stream, such as a received"
+                                                + " Buffer the route did not 
change, is sent as a Buffer. A POJO that is not"
                                                 + " Serializable (without a 
codec registered for it) or no body fails:"
                                                 + " convert it first"),
            producerOut = @BodyType(value = Carrier.UNKNOWN,
@@ -143,7 +146,8 @@ public class VertxEndpoint extends DefaultEndpoint 
implements AsyncEndpoint, Mul
     }
 
     /**
-     * Whether to use publish/subscribe instead of point to point when sending 
to a vertx endpoint.
+     * Whether to use publish/subscribe instead of point to point when sending 
to a vertx endpoint. This only applies
+     * when the exchange pattern is InOnly: an InOut exchange is sent as a 
request to one consumer, which replies.
      */
     public void setPubSub(Boolean pubSub) {
         this.pubSub = pubSub;
diff --git 
a/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxProducer.java
 
b/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxProducer.java
index a1b292626c81..f881ec23a7e0 100644
--- 
a/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxProducer.java
+++ 
b/components/camel-vertx/camel-vertx/src/main/java/org/apache/camel/component/vertx/VertxProducer.java
@@ -56,7 +56,7 @@ public class VertxProducer extends DefaultAsyncProducer {
         boolean reply = ExchangeHelper.isOutCapable(exchange);
         boolean pubSub = getEndpoint().isPubSub();
 
-        Object body = exchange.getMessage().getBody();
+        Object body = VertxBodyHelper.eventBusBody(exchange);
         if (body != null) {
             if (reply) {
                 LOG.debug("Sending to: {} with body: {}", address, body);
@@ -75,7 +75,10 @@ public class VertxProducer extends DefaultAsyncProducer {
             }
         }
 
-        exchange.setException(new InvalidPayloadRuntimeException(exchange, 
String.class));
+        // the producer sends any body type, but not a message without a body
+        exchange.setException(new InvalidPayloadRuntimeException(
+                exchange, Object.class,
+                new IllegalArgumentException("Cannot send a message without a 
body to the event bus address: " + address)));
         callback.done(true);
         return true;
     }
diff --git 
a/components/camel-vertx/camel-vertx/src/test/java/org/apache/camel/component/vertx/VertxRequestReplyFailureTest.java
 
b/components/camel-vertx/camel-vertx/src/test/java/org/apache/camel/component/vertx/VertxRequestReplyFailureTest.java
index 72327afbae70..f337c6578639 100644
--- 
a/components/camel-vertx/camel-vertx/src/test/java/org/apache/camel/component/vertx/VertxRequestReplyFailureTest.java
+++ 
b/components/camel-vertx/camel-vertx/src/test/java/org/apache/camel/component/vertx/VertxRequestReplyFailureTest.java
@@ -19,16 +19,18 @@ package org.apache.camel.component.vertx;
 import io.vertx.core.eventbus.ReplyException;
 import io.vertx.core.eventbus.ReplyFailure;
 import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadRuntimeException;
 import org.apache.camel.builder.RouteBuilder;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * The reply of a request/reply over the event bus must tell the sender that 
the route failed, and must also be sent
- * when the route returns no body.
+ * when the route returns no body or a body that has no codec on the event bus.
  */
 public class VertxRequestReplyFailureTest extends VertxBaseTestSupport {
 
@@ -51,6 +53,26 @@ public class VertxRequestReplyFailureTest extends 
VertxBaseTestSupport {
         assertNull(out.getMessage().getBody());
     }
 
+    @Test
+    public void testReplyWithoutCodecRepliesWithFailure() {
+        Exchange out = template.request("direct:nocodec", e -> 
e.getIn().setBody("Camel"));
+
+        // a body that has no codec cannot be replied, so the sender gets a 
failure instead of waiting for its timeout
+        ReplyException cause = assertInstanceOf(ReplyException.class, 
out.getException());
+        assertEquals(ReplyFailure.RECIPIENT_FAILURE, cause.failureType());
+        assertEquals(VertxConsumer.FAILURE_CODE, cause.failureCode());
+        assertEquals(VertxConsumer.FAILURE_MESSAGE, cause.getMessage());
+    }
+
+    @Test
+    public void testSendWithoutBodyFails() {
+        Exchange out = template.send("direct:nobody", e -> 
e.getIn().setBody(null));
+
+        InvalidPayloadRuntimeException cause = 
assertInstanceOf(InvalidPayloadRuntimeException.class, out.getException());
+        assertTrue(cause.getMessage().contains("Cannot send a message without 
a body to the event bus address: foo.nobody"),
+                cause.getMessage());
+    }
+
     @Override
     protected RouteBuilder createRouteBuilder() {
         return new RouteBuilder() {
@@ -62,6 +84,13 @@ public class VertxRequestReplyFailureTest extends 
VertxBaseTestSupport {
                 from("direct:empty").to("vertx:foo.empty");
                 from("vertx:foo.empty")
                         .setBody(constant(null));
+
+                from("direct:nocodec").to("vertx:foo.nocodec");
+                from("vertx:foo.nocodec")
+                        // an Object is not Serializable and has no codec on 
the event bus
+                        .setBody(e -> new Object());
+
+                from("direct:nobody").to("vertx:foo.nobody");
             }
         };
     }
diff --git 
a/components/camel-vertx/camel-vertx/src/test/java/org/apache/camel/component/vertx/VertxRequestReplyStreamTest.java
 
b/components/camel-vertx/camel-vertx/src/test/java/org/apache/camel/component/vertx/VertxRequestReplyStreamTest.java
new file mode 100644
index 000000000000..47256e1d8708
--- /dev/null
+++ 
b/components/camel-vertx/camel-vertx/src/test/java/org/apache/camel/component/vertx/VertxRequestReplyStreamTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.vertx;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+import io.vertx.core.buffer.Buffer;
+import org.apache.camel.Exchange;
+import org.apache.camel.StreamCache;
+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.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * A stream has no codec on the event bus, so the consumer replies with its 
content as a Buffer, and the producer sends
+ * it as a Buffer.
+ */
+public class VertxRequestReplyStreamTest extends VertxBaseTestSupport {
+
+    @Test
+    public void testEchoBuffer() throws Exception {
+        // with stream caching enabled, the received Buffer is a stream cache 
that the route does not change
+        MockEndpoint mock = getMockEndpoint("mock:echo");
+        mock.expectedMessageCount(1);
+        mock.message(0).body().isInstanceOf(StreamCache.class);
+
+        // send from the template, as a route would turn the Buffer into a 
stream cache before the producer
+        Exchange out = template.request("vertx:foo.echo", e -> 
e.getIn().setBody(Buffer.buffer("Camel")));
+
+        MockEndpoint.assertIsSatisfied(context);
+        assertNull(out.getException());
+        Buffer reply = assertInstanceOf(Buffer.class, 
out.getMessage().getBody());
+        assertEquals("Camel", reply.toString());
+    }
+
+    @Test
+    public void testReplyInputStream() {
+        Exchange out = template.request("direct:stream", e -> 
e.getIn().setBody("Camel"));
+
+        assertNull(out.getException());
+        Buffer reply = assertInstanceOf(Buffer.class, 
out.getMessage().getBody());
+        assertEquals("Hello Camel", reply.toString());
+    }
+
+    @Test
+    public void testForwardBuffer() {
+        // the route receives the Buffer as a stream cache and sends it on to 
another vertx endpoint
+        Exchange out = template.request("vertx:foo.forward", e -> 
e.getIn().setBody(Buffer.buffer("Camel")));
+
+        assertNull(out.getException());
+        assertEquals("Bye Camel", out.getMessage().getBody(String.class));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("vertx:foo.echo").to("mock:echo");
+
+                from("vertx:foo.forward").to("vertx:foo.bye");
+                from("vertx:foo.bye")
+                        .transform(simple("Bye ${bodyAs(String)}"));
+
+                from("direct:stream").to("vertx:foo.stream");
+                from("vertx:foo.stream")
+                        .setBody(e -> new ByteArrayInputStream(
+                                ("Hello " + 
e.getMessage().getBody(String.class)).getBytes(StandardCharsets.UTF_8)));
+            }
+        };
+    }
+}
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 eddc889d1481..5c3eff78d434 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
@@ -2486,6 +2486,18 @@ When configuring TLS through `SSLContextParameters` with 
a standalone `TrustMana
 When both a `TrustManager` and a keystore are configured on 
`TrustManagersParameters`, the `TrustManager` is now used,
 as with the other components that use `SSLContextParameters`; before, the 
keystore-based trust options were used.
 
+=== camel-vertx - a stream body is sent as a Buffer, and the consumer replies 
with a failure when the body has no codec
+
+When a `vertx` consumer receives a message that expects a reply and the route 
ends with a stream body (such as a
+received `Buffer`, which is a stream with stream caching enabled, that the 
route did not change), the consumer now
+replies with its content as a `Buffer`. When the reply body has no codec on 
the event bus (such as a POJO that is not
+`Serializable`), the consumer now logs the error and sends a failure reply (a 
`ReplyException` with failure type
+`RECIPIENT_FAILURE`, failure code 500 and the message `Exchange processing 
failed`). Before, both sent no reply, and
+the sender waited until its reply timeout (30 seconds by default).
+
+The `vertx` producer now sends a stream body as a `Buffer` too, so a route 
from one `vertx` endpoint to another works
+with stream caching enabled. Before, it failed with `No message codec for 
type`.
+
 === camel-knative-http - a header with several values is sent as several 
headers
 
 A message header whose value is a `java.util.Collection` or an object array 
(as the consumer maps a request header
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/VertxEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/VertxEndpointBuilderFactory.java
index 9c363a1904c1..8efd29af60af 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/VertxEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/VertxEndpointBuilderFactory.java
@@ -45,7 +45,9 @@ public interface VertxEndpointBuilderFactory {
         }
         /**
          * Whether to use publish/subscribe instead of point to point when
-         * sending to a vertx endpoint.
+         * sending to a vertx endpoint. This only applies when the exchange
+         * pattern is InOnly: an InOut exchange is sent as a request to one
+         * consumer, which replies.
          * 
          * The option is a: <code>java.lang.Boolean</code> type.
          * 
@@ -61,7 +63,9 @@ public interface VertxEndpointBuilderFactory {
         }
         /**
          * Whether to use publish/subscribe instead of point to point when
-         * sending to a vertx endpoint.
+         * sending to a vertx endpoint. This only applies when the exchange
+         * pattern is InOnly: an InOut exchange is sent as a request to one
+         * consumer, which replies.
          * 
          * The option will be converted to a <code>java.lang.Boolean</code>
          * type.
@@ -218,7 +222,9 @@ public interface VertxEndpointBuilderFactory {
 
         /**
          * Whether to use publish/subscribe instead of point to point when
-         * sending to a vertx endpoint.
+         * sending to a vertx endpoint. This only applies when the exchange
+         * pattern is InOnly: an InOut exchange is sent as a request to one
+         * consumer, which replies.
          * 
          * The option is a: <code>java.lang.Boolean</code> type.
          * 
@@ -234,7 +240,9 @@ public interface VertxEndpointBuilderFactory {
         }
         /**
          * Whether to use publish/subscribe instead of point to point when
-         * sending to a vertx endpoint.
+         * sending to a vertx endpoint. This only applies when the exchange
+         * pattern is InOnly: an InOut exchange is sent as a request to one
+         * consumer, which replies.
          * 
          * The option will be converted to a <code>java.lang.Boolean</code>
          * type.
@@ -320,7 +328,9 @@ public interface VertxEndpointBuilderFactory {
 
         /**
          * Whether to use publish/subscribe instead of point to point when
-         * sending to a vertx endpoint.
+         * sending to a vertx endpoint. This only applies when the exchange
+         * pattern is InOnly: an InOut exchange is sent as a request to one
+         * consumer, which replies.
          * 
          * The option is a: <code>java.lang.Boolean</code> type.
          * 
@@ -336,7 +346,9 @@ public interface VertxEndpointBuilderFactory {
         }
         /**
          * Whether to use publish/subscribe instead of point to point when
-         * sending to a vertx endpoint.
+         * sending to a vertx endpoint. This only applies when the exchange
+         * pattern is InOnly: an InOut exchange is sent as a request to one
+         * consumer, which replies.
          * 
          * The option will be converted to a <code>java.lang.Boolean</code>
          * type.

Reply via email to