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 1c702a11f7d2 CAMEL-26179: camel-milo - a milo-client write of a null 
body fails the exchange (#28514)
1c702a11f7d2 is described below

commit 1c702a11f7d2b0b648e13ac7c59cb35cb35e067a
Author: Serdar Gökay <[email protected]>
AuthorDate: Tue Oct 27 14:50:51 2026 +0300

    CAMEL-26179: camel-milo - a milo-client write of a null body fails the 
exchange (#28514)
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../camel/catalog/docs/milo-client-component.adoc  |  3 ++
 .../src/main/docs/milo-client-component.adoc       |  3 ++
 .../component/milo/client/MiloClientProducer.java  | 12 ++++-
 .../milo/ProducerOnSharedConnectionTest.java       | 53 ++++++++++++++++++++++
 .../camel/component/milo/WriteClientTest.java      | 24 ++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  8 ++++
 6 files changed, 102 insertions(+), 1 deletion(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc
index 3acad3bb65d1..981b4434f4ff 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc
@@ -171,6 +171,9 @@ and `${body.value.value}` is the value. A producer writes 
the message body to it
               allowedSecurityPolicies: None
 ----
 
+A producer does not write a `null` body: the exchange fails with an 
`InvalidPayloadException`. To write a null
+value, set the body to `Variant.NULL_VALUE` or to a `DataValue` that carries 
it.
+
 === Monitoring several nodes
 
 A consumer can monitor several nodes with the `nodes` option instead of `node` 
(the two options cannot be combined).
diff --git a/components/camel-milo/src/main/docs/milo-client-component.adoc 
b/components/camel-milo/src/main/docs/milo-client-component.adoc
index 3acad3bb65d1..981b4434f4ff 100644
--- a/components/camel-milo/src/main/docs/milo-client-component.adoc
+++ b/components/camel-milo/src/main/docs/milo-client-component.adoc
@@ -171,6 +171,9 @@ and `${body.value.value}` is the value. A producer writes 
the message body to it
               allowedSecurityPolicies: None
 ----
 
+A producer does not write a `null` body: the exchange fails with an 
`InvalidPayloadException`. To write a null
+value, set the body to `Variant.NULL_VALUE` or to a `DataValue` that carries 
it.
+
 === Monitoring several nodes
 
 A consumer can monitor several nodes with the `nodes` option instead of `node` 
(the two options cannot be combined).
diff --git 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientProducer.java
 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientProducer.java
index 6af12dfca3ba..165ad8fc2561 100644
--- 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientProducer.java
+++ 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientProducer.java
@@ -22,6 +22,7 @@ import java.util.stream.Collectors;
 
 import org.apache.camel.AsyncCallback;
 import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
 import org.apache.camel.Message;
 import org.apache.camel.component.milo.MiloConstants;
 import org.apache.camel.support.DefaultAsyncProducer;
@@ -71,10 +72,19 @@ public class MiloClientProducer extends 
DefaultAsyncProducer {
     public boolean process(Exchange exchange, AsyncCallback async) {
         final Message msg = exchange.getIn();
         final Object value = msg.getBody();
+        final boolean read = msg.getHeaders().containsKey(HEADER_NODE_IDS);
+
+        if (value == null && !read && this.methodId == null) {
+            // a write needs a value, a null body is not written
+            exchange.setException(new InvalidPayloadException(
+                    exchange, Object.class, msg, "set the body to 
Variant.NULL_VALUE to write a null value"));
+            async.done(true);
+            return true;
+        }
 
         final CompletableFuture<?> future;
 
-        if (msg.getHeaders().containsKey(HEADER_NODE_IDS)) {
+        if (read) {
             final List<String> nodeIds = msg.getHeader(HEADER_NODE_IDS, 
List.class);
             final List<ExpandedNodeId> expandedNodeIds
                     = 
nodeIds.stream().map(String.class::cast).map(ExpandedNodeId::parse).collect(Collectors.toList());
diff --git 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/ProducerOnSharedConnectionTest.java
 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/ProducerOnSharedConnectionTest.java
index d71c73cb9e98..94f00923c136 100644
--- 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/ProducerOnSharedConnectionTest.java
+++ 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/ProducerOnSharedConnectionTest.java
@@ -46,6 +46,7 @@ import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
 import org.eclipse.milo.opcua.sdk.server.AddressSpace;
 import org.eclipse.milo.opcua.stack.core.StatusCodes;
 import org.eclipse.milo.opcua.stack.core.UaException;
+import org.eclipse.milo.opcua.stack.core.UaSerializationException;
 import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
 import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
 import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
@@ -65,6 +66,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -265,6 +267,57 @@ public class ProducerOnSharedConnectionTest extends 
AbstractMiloServerTest {
         }
     }
 
+    /**
+     * A write of a body that the client cannot encode fails the exchange. It 
does not rebuild the connection: the
+     * consumer of another node reports no loss and keeps receiving values in 
the same session.
+     */
+    @Test
+    @Timeout(value = 60, unit = TimeUnit.SECONDS)
+    void writeOfABodyThatCannotBeEncodedKeepsTheConnection() throws Exception {
+        template.sendBody("milo-server:myitem1", "Foo");
+        template.sendBody("milo-server:myitem2", "Foo");
+        item2Endpoint.expectedMessageCount(1);
+        testBody(item2Endpoint.message(0), assertGoodValue("Foo"));
+        context.getRouteController().startRoute("item2");
+        item2Endpoint.assertIsSatisfied();
+
+        final MiloClientEndpoint endpoint = 
context.getEndpoint(resolve(MILO_CLIENT_BASE + ITEM_1), 
MiloClientEndpoint.class);
+        final MiloClientConnection connection = endpoint.createConnection();
+        try {
+            final OpcUaClient client = connection.getOpcUaClient();
+            final NodeId sessionId = client.getSession().getSessionId();
+
+            // the consumer receives the next value only, no loss report and 
not its value again
+            item2Endpoint.reset();
+            item2Endpoint.expectedMessageCount(1);
+            item2Endpoint.setAssertPeriod(2_000);
+            testBody(item2Endpoint.message(0), assertGoodValue("Bar"));
+
+            final Exchange written = template.send(resolve(MILO_CLIENT_BASE + 
ITEM_1), exchange -> {
+                exchange.getMessage().setBody(Map.of("key", "value"));
+                exchange.getMessage().setHeader(MiloConstants.HEADER_AWAIT, 
true);
+            });
+            
assertNotNull(ObjectHelper.getException(UaSerializationException.class, 
written.getException()),
+                    () -> "Expected a UaSerializationException, but the 
exchange failed with " + written.getException());
+
+            // a write on the same connection still succeeds
+            final Exchange rewritten = template.send(resolve(MILO_CLIENT_BASE 
+ ITEM_1), exchange -> {
+                exchange.getMessage().setBody(new Variant("Bar"));
+                exchange.getMessage().setHeader(MiloConstants.HEADER_AWAIT, 
true);
+            });
+            assertNull(rewritten.getException());
+            assertEquals(StatusCode.GOOD, rewritten.getMessage().getBody());
+
+            template.sendBody("milo-server:myitem2", "Bar");
+            item2Endpoint.assertIsSatisfied();
+
+            assertSame(client, connection.getOpcUaClient());
+            assertEquals(sessionId, client.getSession().getSessionId());
+        } finally {
+            endpoint.releaseConnection(connection);
+        }
+    }
+
     /**
      * A write in flight on a connection that Camel replaces fails with the 
status of its closed connection. Its failure
      * does not rebuild the connection that replaced it.
diff --git 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/WriteClientTest.java
 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/WriteClientTest.java
index e225a6bb253e..d45af43f6303 100644
--- 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/WriteClientTest.java
+++ 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/WriteClientTest.java
@@ -17,6 +17,8 @@
 package org.apache.camel.component.milo;
 
 import org.apache.camel.EndpointInject;
+import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
 import org.apache.camel.Produce;
 import org.apache.camel.ProducerTemplate;
 import org.apache.camel.RoutesBuilder;
@@ -27,10 +29,13 @@ import 
org.eclipse.milo.opcua.stack.core.types.builtin.Variant;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import static org.apache.camel.component.milo.NodeIds.nodeValue;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 
 /**
  * Unit tests for writing from the client side
@@ -169,6 +174,25 @@ public class WriteClientTest extends 
AbstractMiloServerTest {
         MockEndpoint.assertIsSatisfied(context);
     }
 
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    public void testWriteNullBodyFails(final boolean await) throws Exception {
+        // item 1 receives the next write only
+        this.test1Endpoint.setExpectedCount(1);
+        testBody(this.test1Endpoint.message(0), assertGoodValue("Foo1"));
+
+        // send
+        final Exchange exchange = this.producer1.send(e -> {
+            e.getMessage().setBody(null);
+            e.getMessage().setHeader("CamelMiloAwait", await);
+        });
+        sendValue(this.producer1, new Variant("Foo1"));
+
+        // assert
+        assertInstanceOf(InvalidPayloadException.class, 
exchange.getException());
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
     private static void sendValue(final ProducerTemplate producerTemplate, 
final Variant variant) {
         // we always write synchronously since we do need the message order
         producerTemplate.sendBodyAndHeader(variant, "CamelMiloAwait", true);
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 910e33a8f88c..32f58f99964e 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
@@ -2726,6 +2726,14 @@ A `sessionTimeout` configured on the `milo-client` or 
`milo-browse` component (f
 `camel.component.milo-client.sessionTimeout`) did not reach its endpoints, so 
their sessions used the default of Milo,
 120000 milliseconds. The endpoints now use it, unless they set 
`sessionTimeout` themselves.
 
+=== camel-milo - a write of a null body fails the exchange
+
+A `milo-client` producer no longer writes a `null` body; the exchange now 
fails with an `InvalidPayloadException`,
+whether or not the write is awaited. Before, it wrote a null value, which a 
server typically refused with
+`Bad_TypeMismatch`, and an awaited write completed without an error and with 
that status as the body. To write a null
+value, set the body to `Variant.NULL_VALUE` or to a `DataValue` that carries 
it. A method call with a `null` body still
+calls the method without input arguments.
+
 === camel-infinispan - the key option of the producer takes precedence over 
the key header
 
 The `infinispan` and `infinispan-embedded` producers now use the `key` option 
of the endpoint when it is set, and use the

Reply via email to