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

davsclaus pushed a commit to branch CAMEL-26156-pubnub-fire-setstate
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 1b02c7b1682f5374aa90196828455765be04471f
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 17:50:42 2026 +0100

    CAMEL-26156: camel-pubnub - FIRE and SETSTATE with an empty body fail 
without calling PubNub, and PNMessageResult converts to String
    
    FIRE and SETSTATE set an exception and called the callback when the body 
was empty, but
    did not return: the PubNub call still ran and called the callback a second 
time, and
    process returned false. They now throw the CamelException as PUBLISH does, 
so process
    completes the exchange once, synchronously, without calling PubNub.
    
    Add a PubNubConverter so the PNMessageResult body of the consumer converts 
to a String:
    a string message as its text, any other message as JSON.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/catalog/components/pubnub.json    |  2 +-
 .../camel/catalog/docs/pubnub-component.adoc       |  3 +
 .../component/pubnub/PubNubConverterLoader.java    | 60 ++++++++++++++
 .../org/apache/camel/component/pubnub/pubnub.json  |  2 +-
 .../services/org/apache/camel/TypeConverterLoader  |  2 +
 .../src/main/docs/pubnub-component.adoc            |  3 +
 .../camel/component/pubnub/PubNubConverter.java    | 45 +++++++++++
 .../camel/component/pubnub/PubNubEndpoint.java     |  3 +-
 .../camel/component/pubnub/PubNubProducer.java     | 10 +--
 .../component/pubnub/PubNubConverterTest.java      | 63 +++++++++++++++
 .../component/pubnub/PubNubEmptyBodyTest.java      | 92 ++++++++++++++++++++++
 .../component/pubnub/PubNubSubscriberTest.java     |  3 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  9 +++
 13 files changed, 288 insertions(+), 9 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pubnub.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pubnub.json
index e4d1181ad220..8f1b477e5a07 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pubnub.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/pubnub.json
@@ -45,7 +45,7 @@
     "CamelPubNubUUID": { "index": 4, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"String", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "UUID to be used as a device identifier.", 
"constantName": "org.apache.camel.component.pubnub.PubNubConstants#UUID" }
   },
   "bodyTypes": {
-    "consumerOut": { "carriers": [ "pojo" ], "javaType": 
"com.pubnub.api.models.consumer.pubsub.PNMessageResult", "byOption": [ { 
"option": "withPresence", "value": "true", "carriers": [ "pojo" ] } ], 
"description": "The PubNub PNMessageResult; its getMessage() is the payload as 
a Gson JsonElement, and it does not convert to the payload text. With 
withPresence, a presence event comes as a PNPresenceEventResult. The channel 
and timetoken are in the CamelPubNubChannel and CamelPubNubTimeTo [...]
+    "consumerOut": { "carriers": [ "pojo" ], "javaType": 
"com.pubnub.api.models.consumer.pubsub.PNMessageResult", "byOption": [ { 
"option": "withPresence", "value": "true", "carriers": [ "pojo" ] } ], 
"description": "The PubNub PNMessageResult; its getMessage() is the payload as 
a Gson JsonElement. It converts to a String: a string message as its text, any 
other message as JSON. With withPresence, a presence event comes as a 
PNPresenceEventResult. The channel and timetoken are in the Cam [...]
     "producerIn": { "carriers": [ "text", "value", "map", "list", "pojo" ], 
"byOption": [ { "option": "operation", "value": "SETSTATE", "carriers": [ 
"map", "pojo" ] }, { "option": "operation", "value": "GETHISTORY", "carriers": 
[ "any" ] }, { "option": "operation", "value": "GETSTATE", "carriers": [ "any" 
] }, { "option": "operation", "value": "HERENOW", "carriers": [ "any" ] } ], 
"description": "The message to publish or fire, serialized to JSON by the 
PubNub client: text is sent as a  [...]
     "producerOut": { "carriers": [ "unchanged" ], "byOption": [ { "option": 
"operation", "value": "GETHISTORY", "carriers": [ "list", "unchanged" ] }, { 
"option": "operation", "value": "GETSTATE", "carriers": [ "pojo" ], "javaType": 
"com.pubnub.api.models.consumer.presence.PNGetStateResult" }, { "option": 
"operation", "value": "HERENOW", "carriers": [ "pojo" ], "javaType": 
"com.pubnub.api.models.consumer.presence.PNHereNowResult" }, { "option": 
"operation", "value": "SETSTATE", "carriers [...]
   },
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pubnub-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pubnub-component.adoc
index c99ce1395990..68dbe084c4ef 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pubnub-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/pubnub-component.adoc
@@ -64,6 +64,9 @@ Pass the full object as the message payload.
 PubNub will take care of object serialization.
 
 When receiving the message body uses objects provided by the PubNub API.
+A message is a `PNMessageResult`, and `getMessage()` is the payload as a Gson 
`JsonElement`.
+The `PNMessageResult` converts to a `String`, so `${body}` or 
`convertBodyTo(String.class)` gives the payload:
+a string message as its text, and any other message (an object, an array, a 
number) as JSON.
 
 == Examples
 
diff --git 
a/components/camel-pubnub/src/generated/java/org/apache/camel/component/pubnub/PubNubConverterLoader.java
 
b/components/camel-pubnub/src/generated/java/org/apache/camel/component/pubnub/PubNubConverterLoader.java
new file mode 100644
index 000000000000..32df8f30563d
--- /dev/null
+++ 
b/components/camel-pubnub/src/generated/java/org/apache/camel/component/pubnub/PubNubConverterLoader.java
@@ -0,0 +1,60 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.component.pubnub;
+
+import javax.annotation.processing.Generated;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.DeferredContextBinding;
+import org.apache.camel.Exchange;
+import org.apache.camel.TypeConversionException;
+import org.apache.camel.TypeConverterLoaderException;
+import org.apache.camel.spi.TypeConverterLoader;
+import org.apache.camel.spi.TypeConverterRegistry;
+import org.apache.camel.support.SimpleTypeConverter;
+import org.apache.camel.support.TypeConverterSupport;
+import org.apache.camel.util.DoubleMap;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.TypeConverterLoaderGeneratorMojo")
+@SuppressWarnings("unchecked")
+@DeferredContextBinding
+public final class PubNubConverterLoader implements TypeConverterLoader, 
CamelContextAware {
+
+    private CamelContext camelContext;
+
+    public PubNubConverterLoader() {
+    }
+
+    @Override
+    public void setCamelContext(CamelContext camelContext) {
+        this.camelContext = camelContext;
+    }
+
+    @Override
+    public CamelContext getCamelContext() {
+        return camelContext;
+    }
+
+    @Override
+    public void load(TypeConverterRegistry registry) throws 
TypeConverterLoaderException {
+        registerConverters(registry);
+    }
+
+    private void registerConverters(TypeConverterRegistry registry) {
+        addTypeConverter(registry, java.lang.String.class, 
com.pubnub.api.models.consumer.pubsub.PNMessageResult.class, false,
+            (type, exchange, value) -> {
+                Object answer = 
org.apache.camel.component.pubnub.PubNubConverter.toString((com.pubnub.api.models.consumer.pubsub.PNMessageResult)
 value);
+                if (false && answer == null) {
+                    answer = Void.class;
+                }
+                return answer;
+            });
+    }
+
+    private static void addTypeConverter(TypeConverterRegistry registry, 
Class<?> toType, Class<?> fromType, boolean allowNull, 
SimpleTypeConverter.ConversionMethod method) {
+        registry.addTypeConverter(toType, fromType, new 
SimpleTypeConverter(allowNull, method));
+    }
+}
diff --git 
a/components/camel-pubnub/src/generated/resources/META-INF/org/apache/camel/component/pubnub/pubnub.json
 
b/components/camel-pubnub/src/generated/resources/META-INF/org/apache/camel/component/pubnub/pubnub.json
index e4d1181ad220..8f1b477e5a07 100644
--- 
a/components/camel-pubnub/src/generated/resources/META-INF/org/apache/camel/component/pubnub/pubnub.json
+++ 
b/components/camel-pubnub/src/generated/resources/META-INF/org/apache/camel/component/pubnub/pubnub.json
@@ -45,7 +45,7 @@
     "CamelPubNubUUID": { "index": 4, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"String", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "UUID to be used as a device identifier.", 
"constantName": "org.apache.camel.component.pubnub.PubNubConstants#UUID" }
   },
   "bodyTypes": {
-    "consumerOut": { "carriers": [ "pojo" ], "javaType": 
"com.pubnub.api.models.consumer.pubsub.PNMessageResult", "byOption": [ { 
"option": "withPresence", "value": "true", "carriers": [ "pojo" ] } ], 
"description": "The PubNub PNMessageResult; its getMessage() is the payload as 
a Gson JsonElement, and it does not convert to the payload text. With 
withPresence, a presence event comes as a PNPresenceEventResult. The channel 
and timetoken are in the CamelPubNubChannel and CamelPubNubTimeTo [...]
+    "consumerOut": { "carriers": [ "pojo" ], "javaType": 
"com.pubnub.api.models.consumer.pubsub.PNMessageResult", "byOption": [ { 
"option": "withPresence", "value": "true", "carriers": [ "pojo" ] } ], 
"description": "The PubNub PNMessageResult; its getMessage() is the payload as 
a Gson JsonElement. It converts to a String: a string message as its text, any 
other message as JSON. With withPresence, a presence event comes as a 
PNPresenceEventResult. The channel and timetoken are in the Cam [...]
     "producerIn": { "carriers": [ "text", "value", "map", "list", "pojo" ], 
"byOption": [ { "option": "operation", "value": "SETSTATE", "carriers": [ 
"map", "pojo" ] }, { "option": "operation", "value": "GETHISTORY", "carriers": 
[ "any" ] }, { "option": "operation", "value": "GETSTATE", "carriers": [ "any" 
] }, { "option": "operation", "value": "HERENOW", "carriers": [ "any" ] } ], 
"description": "The message to publish or fire, serialized to JSON by the 
PubNub client: text is sent as a  [...]
     "producerOut": { "carriers": [ "unchanged" ], "byOption": [ { "option": 
"operation", "value": "GETHISTORY", "carriers": [ "list", "unchanged" ] }, { 
"option": "operation", "value": "GETSTATE", "carriers": [ "pojo" ], "javaType": 
"com.pubnub.api.models.consumer.presence.PNGetStateResult" }, { "option": 
"operation", "value": "HERENOW", "carriers": [ "pojo" ], "javaType": 
"com.pubnub.api.models.consumer.presence.PNHereNowResult" }, { "option": 
"operation", "value": "SETSTATE", "carriers [...]
   },
diff --git 
a/components/camel-pubnub/src/generated/resources/META-INF/services/org/apache/camel/TypeConverterLoader
 
b/components/camel-pubnub/src/generated/resources/META-INF/services/org/apache/camel/TypeConverterLoader
new file mode 100644
index 000000000000..851225edb141
--- /dev/null
+++ 
b/components/camel-pubnub/src/generated/resources/META-INF/services/org/apache/camel/TypeConverterLoader
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+org.apache.camel.component.pubnub.PubNubConverterLoader
diff --git a/components/camel-pubnub/src/main/docs/pubnub-component.adoc 
b/components/camel-pubnub/src/main/docs/pubnub-component.adoc
index c99ce1395990..68dbe084c4ef 100644
--- a/components/camel-pubnub/src/main/docs/pubnub-component.adoc
+++ b/components/camel-pubnub/src/main/docs/pubnub-component.adoc
@@ -64,6 +64,9 @@ Pass the full object as the message payload.
 PubNub will take care of object serialization.
 
 When receiving the message body uses objects provided by the PubNub API.
+A message is a `PNMessageResult`, and `getMessage()` is the payload as a Gson 
`JsonElement`.
+The `PNMessageResult` converts to a `String`, so `${body}` or 
`convertBodyTo(String.class)` gives the payload:
+a string message as its text, and any other message (an object, an array, a 
number) as JSON.
 
 == Examples
 
diff --git 
a/components/camel-pubnub/src/main/java/org/apache/camel/component/pubnub/PubNubConverter.java
 
b/components/camel-pubnub/src/main/java/org/apache/camel/component/pubnub/PubNubConverter.java
new file mode 100644
index 000000000000..bdd908389c5f
--- /dev/null
+++ 
b/components/camel-pubnub/src/main/java/org/apache/camel/component/pubnub/PubNubConverter.java
@@ -0,0 +1,45 @@
+/*
+ * 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.pubnub;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.pubnub.api.models.consumer.pubsub.PNMessageResult;
+import org.apache.camel.Converter;
+
+/**
+ * Converts the PubNub objects the consumer sets as the message body.
+ */
+@Converter(generateLoader = true)
+public final class PubNubConverter {
+
+    private PubNubConverter() {
+    }
+
+    /**
+     * The payload of a received PubNub message as text: a string message as 
its text, any other message (a JSON object,
+     * array, number or boolean) as JSON.
+     */
+    @Converter
+    public static String toString(PNMessageResult message) {
+        JsonElement payload = message.getMessage();
+        if (payload instanceof JsonPrimitive primitive && 
primitive.isString()) {
+            return primitive.getAsString();
+        }
+        return payload.toString();
+    }
+}
diff --git 
a/components/camel-pubnub/src/main/java/org/apache/camel/component/pubnub/PubNubEndpoint.java
 
b/components/camel-pubnub/src/main/java/org/apache/camel/component/pubnub/PubNubEndpoint.java
index 24e56b3438ad..d8ab8417d30f 100644
--- 
a/components/camel-pubnub/src/main/java/org/apache/camel/component/pubnub/PubNubEndpoint.java
+++ 
b/components/camel-pubnub/src/main/java/org/apache/camel/component/pubnub/PubNubEndpoint.java
@@ -42,7 +42,8 @@ import org.apache.camel.support.DefaultEndpoint;
                                    byOption = @BodyType.When(option = 
"withPresence", value = "true",
                                                              carriers = 
Carrier.POJO),
                                    description = "The PubNub PNMessageResult; 
its getMessage() is the payload as a Gson"
-                                                 + " JsonElement, and it does 
not convert to the payload text. With"
+                                                 + " JsonElement. It converts 
to a String: a string message as its text,"
+                                                 + " any other message as 
JSON. With"
                                                  + " withPresence, a presence 
event comes as a PNPresenceEventResult. The"
                                                  + " channel and timetoken are 
in the CamelPubNubChannel and"
                                                  + " CamelPubNubTimeToken 
headers"),
diff --git 
a/components/camel-pubnub/src/main/java/org/apache/camel/component/pubnub/PubNubProducer.java
 
b/components/camel-pubnub/src/main/java/org/apache/camel/component/pubnub/PubNubProducer.java
index 54c0cb402169..3af2d7bcc5fe 100644
--- 
a/components/camel-pubnub/src/main/java/org/apache/camel/component/pubnub/PubNubProducer.java
+++ 
b/components/camel-pubnub/src/main/java/org/apache/camel/component/pubnub/PubNubProducer.java
@@ -123,11 +123,10 @@ public class PubNubProducer extends DefaultAsyncProducer {
                 });
     }
 
-    private void doFire(Exchange exchange, AsyncCallback callback) {
+    private void doFire(Exchange exchange, AsyncCallback callback) throws 
CamelException {
         Object body = exchange.getIn().getBody();
         if (ObjectHelper.isEmpty(body)) {
-            exchange.setException(new CamelException("Can not fire empty 
message"));
-            callback.done(true);
+            throw new CamelException("Can not fire empty message");
         }
         LOG.debug("Sending message [{}] to channel [{}]", body, 
getChannel(exchange));
         endpoint.getPubnub()
@@ -171,11 +170,10 @@ public class PubNubProducer extends DefaultAsyncProducer {
                 });
     }
 
-    private void doSetState(Exchange exchange, AsyncCallback callback) {
+    private void doSetState(Exchange exchange, AsyncCallback callback) throws 
CamelException {
         Object body = exchange.getIn().getBody();
         if (ObjectHelper.isEmpty(body)) {
-            exchange.setException(new CamelException("Can not publish empty 
message"));
-            callback.done(true);
+            throw new CamelException("Can not publish empty message");
         }
         LOG.debug("Sending setState [{}] to channel [{}]", body, 
getChannel(exchange));
         endpoint.getPubnub()
diff --git 
a/components/camel-pubnub/src/test/java/org/apache/camel/component/pubnub/PubNubConverterTest.java
 
b/components/camel-pubnub/src/test/java/org/apache/camel/component/pubnub/PubNubConverterTest.java
new file mode 100644
index 000000000000..1f898f5e2bc0
--- /dev/null
+++ 
b/components/camel-pubnub/src/test/java/org/apache/camel/component/pubnub/PubNubConverterTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.pubnub;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.pubnub.api.models.consumer.pubsub.BasePubSubResult;
+import com.pubnub.api.models.consumer.pubsub.PNMessageResult;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class PubNubConverterTest extends CamelTestSupport {
+
+    @Test
+    public void testStringMessageToString() {
+        assertEquals("Hello World", convert(new JsonPrimitive("Hello World")));
+    }
+
+    @Test
+    public void testObjectMessageToString() {
+        JsonObject json = new JsonObject();
+        json.addProperty("text", "Message");
+        json.addProperty("count", 3);
+        assertEquals("{\"text\":\"Message\",\"count\":3}", convert(json));
+    }
+
+    @Test
+    public void testArrayMessageToString() {
+        JsonArray json = new JsonArray();
+        json.add("a");
+        json.add(1);
+        assertEquals("[\"a\",1]", convert(json));
+    }
+
+    @Test
+    public void testNumberMessageToString() {
+        assertEquals("42", convert(new JsonPrimitive(42)));
+    }
+
+    private String convert(JsonElement payload) {
+        PNMessageResult message = new PNMessageResult(
+                new BasePubSubResult("mychannel", null, 14607577960932487L, 
null, "publisher"), payload, null, null);
+        return context.getTypeConverter().convertTo(String.class, message);
+    }
+}
diff --git 
a/components/camel-pubnub/src/test/java/org/apache/camel/component/pubnub/PubNubEmptyBodyTest.java
 
b/components/camel-pubnub/src/test/java/org/apache/camel/component/pubnub/PubNubEmptyBodyTest.java
new file mode 100644
index 000000000000..69ab08fc4927
--- /dev/null
+++ 
b/components/camel-pubnub/src/test/java/org/apache/camel/component/pubnub/PubNubEmptyBodyTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.pubnub;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.AsyncProducer;
+import org.apache.camel.CamelException;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * FIRE and SETSTATE with an empty body fail without calling PubNub, like 
PUBLISH.
+ */
+public class PubNubEmptyBodyTest extends PubNubTestBase {
+
+    @Test
+    public void testFireNullBody() {
+        Exchange exchange = template.request("direct:fire", e -> 
e.getIn().setBody(null));
+
+        CamelException cause = assertInstanceOf(CamelException.class, 
exchange.getException());
+        assertEquals("Can not fire empty message", cause.getMessage());
+        verify(0, anyRequestedFor(anyUrl()));
+    }
+
+    @Test
+    public void testSetStateEmptyBody() {
+        Exchange exchange = template.request("direct:setstate", e -> 
e.getIn().setBody(Map.of()));
+
+        CamelException cause = assertInstanceOf(CamelException.class, 
exchange.getException());
+        assertEquals("Can not publish empty message", cause.getMessage());
+        verify(0, anyRequestedFor(anyUrl()));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "FIRE", "SETSTATE" })
+    public void testEmptyBodyCompletesSynchronouslyOnce(String operation) 
throws Exception {
+        AsyncProducer producer = 
context.getEndpoint("pubnub:someChannel?uuid=myuuid&pubnub=#pubnub").createAsyncProducer();
+        producer.start();
+        try {
+            Exchange exchange = new DefaultExchange(context);
+            exchange.getIn().setHeader(PubNubConstants.OPERATION, operation);
+            AtomicInteger doneSync = new AtomicInteger();
+            AtomicInteger doneAsync = new AtomicInteger();
+
+            boolean sync = producer.process(exchange, ds -> (ds ? doneSync : 
doneAsync).incrementAndGet());
+
+            assertTrue(sync, "process must return true as the callback was 
called synchronously");
+            assertEquals(1, doneSync.get());
+            assertEquals(0, doneAsync.get());
+            assertInstanceOf(CamelException.class, exchange.getException());
+        } finally {
+            producer.stop();
+        }
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                
from("direct:fire").to("pubnub:someChannel?operation=fire&pubnub=#pubnub");
+                
from("direct:setstate").to("pubnub:someChannel?operation=setstate&uuid=myuuid&pubnub=#pubnub");
+            }
+        };
+    }
+}
diff --git 
a/components/camel-pubnub/src/test/java/org/apache/camel/component/pubnub/PubNubSubscriberTest.java
 
b/components/camel-pubnub/src/test/java/org/apache/camel/component/pubnub/PubNubSubscriberTest.java
index 5caf4c9cdda8..382d9b82b572 100644
--- 
a/components/camel-pubnub/src/test/java/org/apache/camel/component/pubnub/PubNubSubscriberTest.java
+++ 
b/components/camel-pubnub/src/test/java/org/apache/camel/component/pubnub/PubNubSubscriberTest.java
@@ -25,6 +25,7 @@ import static 
com.github.tomakehurst.wiremock.client.WireMock.aResponse;
 import static com.github.tomakehurst.wiremock.client.WireMock.get;
 import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
 import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 public class PubNubSubscriberTest extends PubNubTestBase {
 
@@ -45,6 +46,8 @@ public class PubNubSubscriberTest extends PubNubTestBase {
         mockResult.expectedMessageCount(1);
         mockResult.expectedHeaderReceived(PubNubConstants.CHANNEL, 
"mychannel");
         MockEndpoint.assertIsSatisfied(context);
+        // the PNMessageResult body converts to the JSON of the message
+        assertEquals("{\"text\":\"Message\"}", 
mockResult.getReceivedExchanges().get(0).getIn().getBody(String.class));
     }
 
     @Override
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 1bc188419d61..4aacd03a9ed6 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
@@ -895,6 +895,15 @@ the same JVM.
 An application that relied on the providers being de-registered on shutdown 
must now remove them itself with
 `Security.removeProvider("BC")` and `Security.removeProvider("BCPQC")`.
 
+=== camel-pubnub - FIRE and SETSTATE with an empty body fail without calling 
PubNub
+
+The `FIRE` and `SETSTATE` operations with an empty body now fail with a 
`CamelException` (`Can not fire empty
+message` or `Can not publish empty message`) and do not call PubNub, as 
`PUBLISH` does. Before, the call to PubNub
+still ran, so `FIRE` with a null body could end with the `PubNubException` 
`Message Missing` instead.
+
+The `PNMessageResult` body of the consumer now converts to a `String`: a 
string message as its text, any other message
+as JSON. Before, the conversion gave the `toString()` of the `PNMessageResult` 
object.
+
 === camel-jbang - camel validate normalize writes one file per input file to 
an output directory
 
 `camel validate normalize <files> --output=<dir>` now writes one normalized 
file per input file into the

Reply via email to