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 2d8b6e0da222 CAMEL-26182: camel-mina - sync=false is one-way: the 
producer keeps the message and the consumer writes no reply, and a textline 
consumer converts the reply to a String (#29010)
2d8b6e0da222 is described below

commit 2d8b6e0da222abc654d297ed153d20857cbf3cfc
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 3 11:41:52 2026 +0100

    CAMEL-26182: camel-mina - sync=false is one-way: the producer keeps the 
message and the consumer writes no reply, and a textline consumer converts the 
reply to a String (#29010)
    
    * CAMEL-26182: camel-mina - sync=false is one-way: the producer keeps t…
    
    …he message and the consumer writes no reply, and a textline consumer 
converts the reply to a String
    
    With sync=false the producer read the close-session header with 
exchange.getOut(), which
    replaced the message of an InOut exchange with an empty one. It now uses 
getMessage().
    
    The consumer wrote the body back to the client also with sync=false; it now 
replies only
    with sync=true. With textline=true the consumer now converts the reply to a 
String, as the
    producer does, instead of writing a byte[] or stream as its toString(). The 
consumer now
    honours disconnectOnNoReply and noReplyLogLevel when there is no reply.
    
    The docs explain that a byte[] reply with the default object codec needs
    objectCodecPattern=[B on the producer, and the upgrade guide lists the 
changes.
    
    * Merge remote-tracking branch 'origin/main' into CAMEL-26182-mina-sync
    
    # Conflicts:
    #       docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/components/mina.json  |   4 +-
 .../apache/camel/catalog/docs/mina-component.adoc  |  18 ++-
 .../org/apache/camel/component/mina/mina.json      |   4 +-
 .../camel-mina/src/main/docs/mina-component.adoc   |  18 ++-
 .../apache/camel/component/mina/MinaConsumer.java  |  61 +++++----
 .../apache/camel/component/mina/MinaEndpoint.java  |  22 ++--
 .../apache/camel/component/mina/MinaProducer.java  |  10 +-
 .../component/mina/MinaConsumerReplyTest.java      | 146 +++++++++++++++++++++
 .../camel/component/mina/MinaSyncFalseTest.java    | 113 ++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  15 +++
 10 files changed, 362 insertions(+), 49 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/mina.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/mina.json
index 10b6c4d37ed4..8ae54d3daea6 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/mina.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/mina.json
@@ -63,9 +63,9 @@
   },
   "bodyTypes": {
     "consumerOut": { "carriers": [ "text" ], "javaType": "java.lang.String", 
"dependsOn": [ "codec", "objectCodecPattern", "transferExchange" ], "byOption": 
[ { "option": "protocol", "value": "udp", "carriers": [ "bytes" ], "javaType": 
"byte[]" }, { "option": "allowDefaultCodec", "value": "false", "carriers": [ 
"pojo" ], "javaType": "org.apache.mina.core.buffer.IoBuffer" } ], 
"description": "The data received as a String: one line with textline=true. The 
default object codec over TCP acc [...]
-    "consumerReply": { "carriers": [ "null", "text" ], "dependsOn": [ "codec", 
"transferExchange" ], "byOption": [ { "option": "protocol", "value": "udp", 
"carriers": [ "null", "text", "bytes", "stream", "file" ] }, { "option": 
"allowDefaultCodec", "value": "false", "carriers": [ "null", "pojo" ], 
"javaType": "org.apache.mina.core.buffer.IoBuffer" } ], "description": "The 
body the route ends with is written back to the caller, also with sync=false. 
Send a String: with textline=true other [...]
+    "consumerReply": { "carriers": [ "null", "text" ], "dependsOn": [ "codec", 
"transferExchange" ], "byOption": [ { "option": "textline", "value": "true", 
"carriers": [ "any" ] }, { "option": "protocol", "value": "udp", "carriers": [ 
"null", "text", "bytes", "stream", "file" ] }, { "option": "allowDefaultCodec", 
"value": "false", "carriers": [ "null", "pojo" ], "javaType": 
"org.apache.mina.core.buffer.IoBuffer" } ], "description": "With sync=true (the 
default) the body the route ends wi [...]
     "producerIn": { "carriers": [ "null", "text" ], "dependsOn": [ "codec", 
"transferExchange" ], "byOption": [ { "option": "textline", "value": "true", 
"carriers": [ "any" ] }, { "option": "protocol", "value": "udp", "carriers": [ 
"null", "text", "bytes", "stream", "file" ] }, { "option": "allowDefaultCodec", 
"value": "false", "carriers": [ "null", "pojo" ], "javaType": 
"org.apache.mina.core.buffer.IoBuffer" } ], "description": "The data to send: a 
String. The default object codec over  [...]
-    "producerOut": { "carriers": [ "text", "null" ], "javaType": 
"java.lang.String", "dependsOn": [ "codec", "objectCodecPattern", 
"transferExchange" ], "byOption": [ { "option": "sync", "value": "false", 
"carriers": [ "unchanged", "null" ] }, { "option": "protocol", "value": "udp", 
"carriers": [ "bytes", "null" ], "javaType": "byte[]" }, { "option": 
"allowDefaultCodec", "value": "false", "carriers": [ "pojo", "null" ], 
"javaType": "org.apache.mina.core.buffer.IoBuffer" } ], "description [...]
+    "producerOut": { "carriers": [ "text", "null" ], "javaType": 
"java.lang.String", "dependsOn": [ "codec", "objectCodecPattern", 
"transferExchange" ], "byOption": [ { "option": "sync", "value": "false", 
"carriers": [ "unchanged" ] }, { "option": "protocol", "value": "udp", 
"carriers": [ "bytes", "null" ], "javaType": "byte[]" }, { "option": 
"allowDefaultCodec", "value": "false", "carriers": [ "pojo", "null" ], 
"javaType": "org.apache.mina.core.buffer.IoBuffer" } ], "description": "With 
[...]
   },
   "properties": {
     "protocol": { "index": 0, "kind": "path", "displayName": "Protocol", 
"group": "common", "label": "", "required": true, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.mina.MinaConfiguration", "configurationField": 
"configuration", "description": "Protocol to use" },
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mina-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mina-component.adoc
index 0b4c3786da8b..ffe5f7d90073 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mina-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mina-component.adoc
@@ -21,8 +21,9 @@ Favor using xref:netty-component.adoc[Netty] as Netty is a 
much more active
 maintained and popular project than Apache Mina currently is.
 ====
 
-WARNING: Be careful with `sync=false` on consumer endpoints. In camel-mina, all
-consumer exchanges are `InOut`. This is different to camel-mina.
+NOTE: With `sync=false` the endpoint is one-way: the consumer does not write a 
reply back to the client, and the
+producer does not wait for a reply and leaves the message of the exchange as 
it was. Use `sync=true` (the default) for
+request-reply.
 
 Maven users will need to add the following dependency to their `pom.xml`
 for this component:
@@ -82,6 +83,19 @@ Registry; for example, by creating a bean in the
 Spring XML file. Then use the `codec` option to specify the bean ID of
 your codec. See xref:dataformats:hl7-dataformat.adoc[HL7] that has a custom 
codec.
 
+=== The reply of the consumer
+
+With `sync=true` (the default) the consumer writes the body the route ends 
with back to the client. With
+`textline=true` the body is converted to a `String` first, so a `byte[]` or a 
stream is written as its text.
+
+The default object codec (no `textline` and no `codec`) only deserializes the 
classes that `objectCodecPattern` lists,
+and a `String`. This applies to the reply as well: a producer that should 
receive a `byte[]` reply needs
+`objectCodecPattern=[B`. Without it the producer fails to decode the reply 
with a `StreamCorruptedException` (such as
+`invalid type code: 42`). Keep `objectCodecPattern` as narrow as the replies 
need.
+
+When the route ends with no body, the consumer writes nothing, logs it at 
`noReplyLogLevel` and closes the session, so
+the client does not wait for a reply. Set `disconnectOnNoReply=false` to keep 
the session open.
+
 === Get the IoSession for message
 
 You can get the IoSession from the message header with this key
diff --git 
a/components/camel-mina/src/generated/resources/META-INF/org/apache/camel/component/mina/mina.json
 
b/components/camel-mina/src/generated/resources/META-INF/org/apache/camel/component/mina/mina.json
index 10b6c4d37ed4..8ae54d3daea6 100644
--- 
a/components/camel-mina/src/generated/resources/META-INF/org/apache/camel/component/mina/mina.json
+++ 
b/components/camel-mina/src/generated/resources/META-INF/org/apache/camel/component/mina/mina.json
@@ -63,9 +63,9 @@
   },
   "bodyTypes": {
     "consumerOut": { "carriers": [ "text" ], "javaType": "java.lang.String", 
"dependsOn": [ "codec", "objectCodecPattern", "transferExchange" ], "byOption": 
[ { "option": "protocol", "value": "udp", "carriers": [ "bytes" ], "javaType": 
"byte[]" }, { "option": "allowDefaultCodec", "value": "false", "carriers": [ 
"pojo" ], "javaType": "org.apache.mina.core.buffer.IoBuffer" } ], 
"description": "The data received as a String: one line with textline=true. The 
default object codec over TCP acc [...]
-    "consumerReply": { "carriers": [ "null", "text" ], "dependsOn": [ "codec", 
"transferExchange" ], "byOption": [ { "option": "protocol", "value": "udp", 
"carriers": [ "null", "text", "bytes", "stream", "file" ] }, { "option": 
"allowDefaultCodec", "value": "false", "carriers": [ "null", "pojo" ], 
"javaType": "org.apache.mina.core.buffer.IoBuffer" } ], "description": "The 
body the route ends with is written back to the caller, also with sync=false. 
Send a String: with textline=true other [...]
+    "consumerReply": { "carriers": [ "null", "text" ], "dependsOn": [ "codec", 
"transferExchange" ], "byOption": [ { "option": "textline", "value": "true", 
"carriers": [ "any" ] }, { "option": "protocol", "value": "udp", "carriers": [ 
"null", "text", "bytes", "stream", "file" ] }, { "option": "allowDefaultCodec", 
"value": "false", "carriers": [ "null", "pojo" ], "javaType": 
"org.apache.mina.core.buffer.IoBuffer" } ], "description": "With sync=true (the 
default) the body the route ends wi [...]
     "producerIn": { "carriers": [ "null", "text" ], "dependsOn": [ "codec", 
"transferExchange" ], "byOption": [ { "option": "textline", "value": "true", 
"carriers": [ "any" ] }, { "option": "protocol", "value": "udp", "carriers": [ 
"null", "text", "bytes", "stream", "file" ] }, { "option": "allowDefaultCodec", 
"value": "false", "carriers": [ "null", "pojo" ], "javaType": 
"org.apache.mina.core.buffer.IoBuffer" } ], "description": "The data to send: a 
String. The default object codec over  [...]
-    "producerOut": { "carriers": [ "text", "null" ], "javaType": 
"java.lang.String", "dependsOn": [ "codec", "objectCodecPattern", 
"transferExchange" ], "byOption": [ { "option": "sync", "value": "false", 
"carriers": [ "unchanged", "null" ] }, { "option": "protocol", "value": "udp", 
"carriers": [ "bytes", "null" ], "javaType": "byte[]" }, { "option": 
"allowDefaultCodec", "value": "false", "carriers": [ "pojo", "null" ], 
"javaType": "org.apache.mina.core.buffer.IoBuffer" } ], "description [...]
+    "producerOut": { "carriers": [ "text", "null" ], "javaType": 
"java.lang.String", "dependsOn": [ "codec", "objectCodecPattern", 
"transferExchange" ], "byOption": [ { "option": "sync", "value": "false", 
"carriers": [ "unchanged" ] }, { "option": "protocol", "value": "udp", 
"carriers": [ "bytes", "null" ], "javaType": "byte[]" }, { "option": 
"allowDefaultCodec", "value": "false", "carriers": [ "pojo", "null" ], 
"javaType": "org.apache.mina.core.buffer.IoBuffer" } ], "description": "With 
[...]
   },
   "properties": {
     "protocol": { "index": 0, "kind": "path", "displayName": "Protocol", 
"group": "common", "label": "", "required": true, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.mina.MinaConfiguration", "configurationField": 
"configuration", "description": "Protocol to use" },
diff --git a/components/camel-mina/src/main/docs/mina-component.adoc 
b/components/camel-mina/src/main/docs/mina-component.adoc
index 0b4c3786da8b..ffe5f7d90073 100644
--- a/components/camel-mina/src/main/docs/mina-component.adoc
+++ b/components/camel-mina/src/main/docs/mina-component.adoc
@@ -21,8 +21,9 @@ Favor using xref:netty-component.adoc[Netty] as Netty is a 
much more active
 maintained and popular project than Apache Mina currently is.
 ====
 
-WARNING: Be careful with `sync=false` on consumer endpoints. In camel-mina, all
-consumer exchanges are `InOut`. This is different to camel-mina.
+NOTE: With `sync=false` the endpoint is one-way: the consumer does not write a 
reply back to the client, and the
+producer does not wait for a reply and leaves the message of the exchange as 
it was. Use `sync=true` (the default) for
+request-reply.
 
 Maven users will need to add the following dependency to their `pom.xml`
 for this component:
@@ -82,6 +83,19 @@ Registry; for example, by creating a bean in the
 Spring XML file. Then use the `codec` option to specify the bean ID of
 your codec. See xref:dataformats:hl7-dataformat.adoc[HL7] that has a custom 
codec.
 
+=== The reply of the consumer
+
+With `sync=true` (the default) the consumer writes the body the route ends 
with back to the client. With
+`textline=true` the body is converted to a `String` first, so a `byte[]` or a 
stream is written as its text.
+
+The default object codec (no `textline` and no `codec`) only deserializes the 
classes that `objectCodecPattern` lists,
+and a `String`. This applies to the reply as well: a producer that should 
receive a `byte[]` reply needs
+`objectCodecPattern=[B`. Without it the producer fails to decode the reply 
with a `StreamCorruptedException` (such as
+`invalid type code: 42`). Keep `objectCodecPattern` as narrow as the replies 
need.
+
+When the route ends with no body, the consumer writes nothing, logs it at 
`noReplyLogLevel` and closes the session, so
+the client does not wait for a reply. Set `disconnectOnNoReply=false` to keep 
the session open.
+
 === Get the IoSession for message
 
 You can get the IoSession from the message header with this key
diff --git 
a/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaConsumer.java
 
b/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaConsumer.java
index cb9350edaea4..4550b8ae8a4f 100644
--- 
a/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaConsumer.java
+++ 
b/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaConsumer.java
@@ -29,6 +29,7 @@ import org.apache.camel.Exchange;
 import org.apache.camel.ExchangePattern;
 import org.apache.camel.ExchangePropertyKey;
 import org.apache.camel.Processor;
+import org.apache.camel.spi.CamelLogger;
 import org.apache.camel.support.DefaultConsumer;
 import org.apache.camel.support.ExchangeHelper;
 import org.apache.camel.util.IOHelper;
@@ -70,6 +71,7 @@ public class MinaConsumer extends DefaultConsumer {
     private SocketAddress address;
     private IoAcceptor acceptor;
     private final MinaConfiguration configuration;
+    private final CamelLogger noReplyLogger;
     private ExecutorService workerPool;
     // the acceptor or connector and the worker pool were released by doStop, 
and are created again on start
     private boolean released;
@@ -77,6 +79,7 @@ public class MinaConsumer extends DefaultConsumer {
     public MinaConsumer(final MinaEndpoint endpoint, Processor processor) 
throws Exception {
         super(endpoint, processor);
         this.configuration = endpoint.getConfiguration();
+        this.noReplyLogger = new CamelLogger(LOG, 
configuration.getNoReplyLogLevel());
         //
         // All mina endpoints are InOut. The endpoints are asynchronous.
         // Endpoints can send "n" messages and receive "m" messages.
@@ -491,34 +494,44 @@ public class MinaConsumer extends DefaultConsumer {
             }
 
             try {
-                //
-                // If there's a response to send, send it.
-                //
                 boolean disconnect = 
getEndpoint().getConfiguration().isDisconnect();
-                Object response;
-                if (exchange.hasOut()) {
-                    response = MinaPayloadHelper.getOut(getEndpoint(), 
exchange);
-                } else {
-                    response = MinaPayloadHelper.getIn(getEndpoint(), 
exchange);
-                }
-
-                boolean failed = exchange.isFailed();
-                if (failed && 
!getEndpoint().getConfiguration().isTransferExchange()) {
-                    if (exchange.getException() != null) {
-                        response = 
getEndpoint().getConfiguration().isMuteException()
-                                ? mutedException() : exchange.getException();
+                // only reply when sync is enabled (request-reply); with 
sync=false the endpoint is one-way
+                if (configuration.isSync()) {
+                    Object response;
+                    if (exchange.hasOut()) {
+                        response = MinaPayloadHelper.getOut(getEndpoint(), 
exchange);
                     } else {
-                        // failed and no exception, must be a fault
-                        response = exchange.getOut().getBody();
+                        response = MinaPayloadHelper.getIn(getEndpoint(), 
exchange);
                     }
-                }
 
-                if (response != null) {
-                    LOG.debug("Writing body: {}", response);
-                    MinaHelper.writeBody(session, response, exchange, 
configuration.getWriteTimeout());
-                } else {
-                    LOG.debug("Writing no response");
-                    disconnect = Boolean.TRUE;
+                    boolean failed = exchange.isFailed();
+                    if (failed && 
!getEndpoint().getConfiguration().isTransferExchange()) {
+                        if (exchange.getException() != null) {
+                            response = 
getEndpoint().getConfiguration().isMuteException()
+                                    ? mutedException() : 
exchange.getException();
+                        } else {
+                            // failed and no exception, must be a fault
+                            response = exchange.getOut().getBody();
+                        }
+                    }
+
+                    if (response != null) {
+                        // if textline enabled then convert to a String which 
must be used for textline
+                        // (the text line encoder would otherwise write a 
byte[] or a stream with its toString)
+                        if (configuration.isTextline()) {
+                            response = 
getEndpoint().getCamelContext().getTypeConverter().mandatoryConvertTo(String.class,
+                                    exchange, response);
+                        }
+                        LOG.debug("Writing body: {}", response);
+                        MinaHelper.writeBody(session, response, exchange, 
configuration.getWriteTimeout());
+                    } else {
+                        noReplyLogger.log("No payload to send as reply for 
exchange: " + exchange);
+                        if (configuration.isDisconnectOnNoReply()) {
+                            // must close session if no data to write 
otherwise client will never receive a response
+                            // and wait forever (if not timing out)
+                            disconnect = true;
+                        }
+                    }
                 }
 
                 // should session be closed after complete?
diff --git 
a/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaEndpoint.java
 
b/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaEndpoint.java
index 0ce86273caf5..2b7b97f9f367 100644
--- 
a/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaEndpoint.java
+++ 
b/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaEndpoint.java
@@ -50,6 +50,7 @@ import org.apache.camel.util.ObjectHelper;
            consumerReply = @BodyType(value = { Carrier.NULL, Carrier.TEXT },
                                      dependsOn = { "codec", "transferExchange" 
},
                                      byOption = {
+                                             @BodyType.When(option = 
"textline", value = "true", carriers = Carrier.ANY),
                                              @BodyType.When(option = 
"protocol", value = "udp",
                                                             carriers = {
                                                                     
Carrier.NULL, Carrier.TEXT, Carrier.BYTES,
@@ -57,12 +58,13 @@ import org.apache.camel.util.ObjectHelper;
                                              @BodyType.When(option = 
"allowDefaultCodec", value = "false",
                                                             carriers = { 
Carrier.NULL, Carrier.POJO },
                                                             javaType = 
"org.apache.mina.core.buffer.IoBuffer") },
-                                     description = "The body the route ends 
with is written back to the caller, also with"
-                                                   + " sync=false. Send a 
String: with textline=true other bodies are"
-                                                   + " written with 
toString(), and the object codec writes any"
-                                                   + " Serializable object, 
which a Mina client accepts only when its"
-                                                   + " objectCodecPattern 
lists the class. No body sends nothing and closes"
-                                                   + " the connection"),
+                                     description = "With sync=true (the 
default) the body the route ends with is written"
+                                                   + " back to the caller; 
with sync=false nothing is written. Send a"
+                                                   + " String: textline=true 
converts any body to a line of text, and the"
+                                                   + " object codec writes any 
Serializable object, which a Mina client"
+                                                   + " accepts only when its 
objectCodecPattern lists the class (a byte"
+                                                   + " array needs [B). No 
body sends nothing and closes the connection"
+                                                   + " (disconnectOnNoReply)"),
            producerIn = @BodyType(value = { Carrier.NULL, Carrier.TEXT },
                                   dependsOn = { "codec", "transferExchange" },
                                   byOption = {
@@ -81,16 +83,16 @@ import org.apache.camel.util.ObjectHelper;
            producerOut = @BodyType(value = { Carrier.TEXT, Carrier.NULL }, 
javaType = "java.lang.String",
                                    dependsOn = { "codec", 
"objectCodecPattern", "transferExchange" },
                                    byOption = {
-                                           @BodyType.When(option = "sync", 
value = "false",
-                                                          carriers = { 
Carrier.UNCHANGED, Carrier.NULL }),
+                                           @BodyType.When(option = "sync", 
value = "false", carriers = Carrier.UNCHANGED),
                                            @BodyType.When(option = "protocol", 
value = "udp",
                                                           carriers = { 
Carrier.BYTES, Carrier.NULL }, javaType = "byte[]"),
                                            @BodyType.When(option = 
"allowDefaultCodec", value = "false",
                                                           carriers = { 
Carrier.POJO, Carrier.NULL },
                                                           javaType = 
"org.apache.mina.core.buffer.IoBuffer") },
                                    description = "With sync=true (the default) 
the reply of the server as a String, or a"
-                                                 + " byte array over UDP. No 
body is not sent and stays null. With"
-                                                 + " sync=false an InOut 
exchange ends with an empty message"))
+                                                 + " byte array over UDP. The 
object codec accepts another reply only when"
+                                                 + " objectCodecPattern lists 
its class (a byte array needs [B). No body is"
+                                                 + " not sent and stays null. 
With sync=false the message is unchanged"))
 public class MinaEndpoint extends DefaultEndpoint implements 
MultipleConsumersSupport, EndpointServiceLocation {
 
     @UriParam
diff --git 
a/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaProducer.java
 
b/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaProducer.java
index 934b4e20db33..764ebd035ce4 100644
--- 
a/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaProducer.java
+++ 
b/components/camel-mina/src/main/java/org/apache/camel/component/mina/MinaProducer.java
@@ -211,13 +211,9 @@ public class MinaProducer extends DefaultProducer {
             return;
         }
 
-        // should session be closed after complete?
-        Boolean close;
-        if (ExchangeHelper.isOutCapable(exchange)) {
-            close = 
exchange.getOut().getHeader(MinaConstants.MINA_CLOSE_SESSION_WHEN_COMPLETE, 
Boolean.class);
-        } else {
-            close = 
exchange.getIn().getHeader(MinaConstants.MINA_CLOSE_SESSION_WHEN_COMPLETE, 
Boolean.class);
-        }
+        // should session be closed after complete? (use getMessage as there 
is no out message with sync=false,
+        // and calling getOut would replace the message with an empty one)
+        Boolean close = 
exchange.getMessage().getHeader(MinaConstants.MINA_CLOSE_SESSION_WHEN_COMPLETE, 
Boolean.class);
 
         // should we disconnect, the header can override the configuration
         boolean disconnect = getEndpoint().getConfiguration().isDisconnect();
diff --git 
a/components/camel-mina/src/test/java/org/apache/camel/component/mina/MinaConsumerReplyTest.java
 
b/components/camel-mina/src/test/java/org/apache/camel/component/mina/MinaConsumerReplyTest.java
new file mode 100644
index 000000000000..9e1a6a0f21c5
--- /dev/null
+++ 
b/components/camel-mina/src/test/java/org/apache/camel/component/mina/MinaConsumerReplyTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.mina;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * The reply a consumer with sync=true writes back: a textline reply that is 
not a String, the
+ * CamelMinaCloseSessionWhenComplete header, disconnectOnNoReply, and a byte[] 
reply with the object codec.
+ */
+public class MinaConsumerReplyTest extends BaseMinaTest {
+
+    private int textlinePort;
+    private int noReplyPort;
+    private int objectPort;
+
+    @Test
+    public void testTextlineBytesReply() {
+        Object out = 
template.requestBody(String.format("mina:tcp://localhost:%d?textline=true", 
textlinePort), "bytes");
+        assertEquals("Bye bytes", out);
+    }
+
+    @Test
+    public void testTextlineStreamReply() {
+        Object out = 
template.requestBody(String.format("mina:tcp://localhost:%d?textline=true", 
textlinePort), "stream");
+        assertEquals("Bye stream", out);
+    }
+
+    @Test
+    public void testCloseSessionWhenCompleteHeader() throws Exception {
+        try (Socket socket = connect(textlinePort)) {
+            BufferedReader reader = send(socket, "close");
+            assertEquals("Bye close", reader.readLine());
+            // the header set by the route closes the session
+            assertNull(reader.readLine());
+        }
+    }
+
+    @Test
+    public void testSessionStaysOpenWithoutCloseHeader() throws Exception {
+        try (Socket socket = connect(textlinePort)) {
+            BufferedReader reader = send(socket, "bytes");
+            assertEquals("Bye bytes", reader.readLine());
+            assertThrows(SocketTimeoutException.class, reader::readLine);
+        }
+    }
+
+    @Test
+    public void testDisconnectOnNoReplyFalse() throws Exception {
+        try (Socket socket = connect(noReplyPort)) {
+            // no reply, and the session stays open
+            BufferedReader reader = send(socket, "London");
+            assertThrows(SocketTimeoutException.class, reader::readLine);
+
+            // so the next request on the same session gets its reply
+            reader = send(socket, "Woodbine");
+            assertEquals("Hello Chad", reader.readLine());
+        }
+    }
+
+    @Test
+    public void testObjectCodecBytesReplyWithObjectCodecPattern() {
+        // the object codec only accepts the classes of objectCodecPattern, so 
a byte[] reply needs [B
+        Object out = 
template.requestBody(String.format("mina:tcp://localhost:%d?objectCodecPattern=[B",
 objectPort),
+                "Hello");
+        assertArrayEquals("Bye Hello".getBytes(StandardCharsets.UTF_8), 
(byte[]) out);
+    }
+
+    private static Socket connect(int port) throws Exception {
+        Socket socket = new Socket();
+        socket.connect(new InetSocketAddress("localhost", port));
+        socket.setSoTimeout(1000);
+        return socket;
+    }
+
+    private static BufferedReader send(Socket socket, String line) throws 
Exception {
+        OutputStream os = socket.getOutputStream();
+        os.write((line + "\n").getBytes(StandardCharsets.US_ASCII));
+        os.flush();
+        return new BufferedReader(new 
InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                textlinePort = getPort();
+                noReplyPort = getNextPort();
+                objectPort = getNextPort();
+
+                
fromF("mina:tcp://localhost:%d?textline=true&encoding=US-ASCII", textlinePort)
+                        .choice()
+                        .when(body().isEqualTo("stream"))
+                        .process(e -> e.getMessage().setBody(
+                                new ByteArrayInputStream("Bye 
stream".getBytes(StandardCharsets.US_ASCII))))
+                        .when(body().isEqualTo("close"))
+                        .setBody(constant("Bye close"))
+                        
.setHeader(MinaConstants.MINA_CLOSE_SESSION_WHEN_COMPLETE, constant(true))
+                        .otherwise()
+                        .process(e -> e.getMessage().setBody(
+                                ("Bye " + 
e.getMessage().getBody(String.class)).getBytes(StandardCharsets.US_ASCII)));
+
+                
fromF("mina:tcp://localhost:%d?textline=true&encoding=US-ASCII&disconnectOnNoReply=false&noReplyLogLevel=OFF",
+                        noReplyPort)
+                        .choice()
+                        .when(body().isEqualTo("Woodbine"))
+                        .transform(constant("Hello Chad"))
+                        .otherwise()
+                        .transform(constant((Object) null));
+
+                fromF("mina:tcp://localhost:%d", objectPort)
+                        .process(e -> e.getMessage().setBody(
+                                ("Bye " + 
e.getMessage().getBody(String.class)).getBytes(StandardCharsets.UTF_8)));
+            }
+        };
+    }
+}
diff --git 
a/components/camel-mina/src/test/java/org/apache/camel/component/mina/MinaSyncFalseTest.java
 
b/components/camel-mina/src/test/java/org/apache/camel/component/mina/MinaSyncFalseTest.java
new file mode 100644
index 000000000000..3132e37bbb63
--- /dev/null
+++ 
b/components/camel-mina/src/test/java/org/apache/camel/component/mina/MinaSyncFalseTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.mina;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.Exchange;
+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.assertThrows;
+
+/**
+ * With sync=false the endpoints are one-way: the producer keeps the message 
of an InOut exchange, and the consumer does
+ * not write a reply.
+ */
+public class MinaSyncFalseTest extends BaseMinaTest {
+
+    private int udpPort;
+
+    @Test
+    public void testInOutProducerKeepsBodyAndHeaders() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:tcp");
+        mock.expectedBodiesReceived("Hello World");
+
+        Exchange out = 
template.request(String.format("mina:tcp://localhost:%d?sync=false&textline=true",
 getPort()), e -> {
+            e.getMessage().setBody("Hello World");
+            e.getMessage().setHeader("foo", "bar");
+        });
+
+        MockEndpoint.assertIsSatisfied(context);
+        assertEquals("Hello World", out.getMessage().getBody());
+        assertEquals("bar", out.getMessage().getHeader("foo"));
+    }
+
+    @Test
+    public void testTcpConsumerDoesNotReply() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:tcp");
+        mock.expectedBodiesReceived("Hello World");
+
+        try (Socket socket = new Socket()) {
+            socket.connect(new InetSocketAddress("localhost", getPort()));
+            socket.setSoTimeout(1000);
+            OutputStream os = socket.getOutputStream();
+            os.write("Hello World\n".getBytes(StandardCharsets.US_ASCII));
+            os.flush();
+
+            MockEndpoint.assertIsSatisfied(context);
+
+            // no reply is written back, and the session stays open
+            BufferedReader reader
+                    = new BufferedReader(new 
InputStreamReader(socket.getInputStream(), StandardCharsets.US_ASCII));
+            assertThrows(SocketTimeoutException.class, reader::readLine);
+        }
+    }
+
+    @Test
+    public void testUdpConsumerDoesNotReply() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:udp");
+        mock.expectedBodiesReceived("Hello World");
+
+        try (DatagramSocket socket = new DatagramSocket()) {
+            socket.setSoTimeout(1000);
+            InetAddress address = InetAddress.getByName("127.0.0.1");
+            byte[] data = "Hello World".getBytes(StandardCharsets.US_ASCII);
+            socket.send(new DatagramPacket(data, data.length, address, 
udpPort));
+
+            MockEndpoint.assertIsSatisfied(context);
+
+            // no datagram is sent back
+            byte[] buf = new byte[128];
+            DatagramPacket receive = new DatagramPacket(buf, buf.length);
+            assertThrows(SocketTimeoutException.class, () -> 
socket.receive(receive));
+        }
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                udpPort = getNextPort();
+
+                fromF("mina:tcp://localhost:%d?sync=false&textline=true", 
getPort()).to("mock:tcp");
+                fromF("mina:udp://127.0.0.1:%d?sync=false", 
udpPort).convertBodyTo(String.class).to("mock:udp");
+            }
+        };
+    }
+}
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 cbe623761a39..41f803bb6a4a 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
@@ -2053,6 +2053,21 @@ does not declare. Before, the asynchronous consumer sent 
the message of the exce
 sent a `TApplicationException` as it was, such as one returned by another 
Thrift service that the route called. An
 exception declared in the IDL is still sent as it is. Set 
`muteException=false` to send the exception as before.
 
+=== camel-mina - sync=false is one-way, and the reply of a textline consumer
+
+A consumer with `sync=false` no longer writes the body the route ends with 
back to the client. Before, it wrote it back
+(over TCP and UDP), so a client that only sends got its own message echoed. 
Use `sync=true` (the default) when the client
+expects a reply.
+
+A producer with `sync=false` now leaves the body and headers of an `InOut` 
exchange as they were. Before, the exchange
+ended with an empty message.
+
+A consumer with `textline=true` now converts the reply to a `String` before 
writing it, as the producer does with the
+request. Before, a `byte[]` or a stream reply was written as its `toString()`.
+
+A consumer with `disconnectOnNoReply=false` now keeps the session open when 
the route ends with no body, and logs it at
+`noReplyLogLevel` (`WARN` by default). Before, the consumer always closed the 
session and logged at `DEBUG`.
+
 === camel-thrift - the reply of the consumer must convert to the return type 
of the method
 
 For a method with a return type, the consumer now fails the call with a 
`TApplicationException` when the route leaves

Reply via email to