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 c3ce839be05f CAMEL-26136: camel-netty - convert replies for the
default codec, fail on useByteBuf, ignore replies with sync=false (#28914)
c3ce839be05f is described below
commit c3ce839be05fb8f26a2683eb8bc775e8029b7cf6
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:43:36 2026 +0100
CAMEL-26136: camel-netty - convert replies for the default codec, fail on
useByteBuf, ignore replies with sync=false (#28914)
…l on useByteBuf, ignore replies with sync=false
With the default codec (String encoder only) a consumer reply that was not a
String, including the exception of a failed exchange, could not be written
and
the client waited. The consumer now converts such a reply to a String, as
textline does, and closes the TCP channel when no reply can be written.
useByteBuf=true used the non-mandatory getBody(ByteBuf.class), so a body
that
could not be converted was skipped silently; it now fails the exchange, and
the consumer reply set as out message honours useByteBuf as well.
With sync=false the producer set a late reply of the server on an exchange
that
had already continued routing (and called its callback a second time); such
messages are now ignored.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../apache/camel/catalog/docs/netty-component.adoc | 10 +++
.../camel-netty/src/main/docs/netty-component.adoc | 10 +++
.../camel/component/netty/NettyConfiguration.java | 10 +++
.../camel/component/netty/NettyPayloadHelper.java | 25 +++---
.../netty/handlers/ClientChannelHandler.java | 9 +++
.../netty/handlers/ServerChannelHandler.java | 44 +++++++++--
.../netty/NettyDefaultCodecReplyTest.java | 91 ++++++++++++++++++++++
.../netty/NettyProducerSyncFalseReplyTest.java | 64 +++++++++++++++
.../camel/component/netty/NettyUseByteBufTest.java | 86 ++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 16 ++++
10 files changed, 348 insertions(+), 17 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc
index 5ce184a15f31..8a8a558565ba 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/netty-component.adoc
@@ -160,6 +160,16 @@ exception thrown by the route, so the reply carries
neither its class name nor i
client receives the line `java.lang.Exception: Exchange processing failed`.
Set `muteException=false` to write the
exception of the route, as in earlier versions.
+The default codec (no `textline`, no custom `encoders` or `decoders`) only
writes text. The consumer therefore converts
+a reply that is not a `String` (or a `ByteBuf`), such as bytes, a stream, a
file, a `Map` or the exception of a failed
+exchange, to a `String` before writing it, as it does with the textline codec.
If no reply can be written, the
+consumer closes the TCP channel, so the client does not wait for a reply that
never comes.
+
+With `useByteBuf=true` a body that cannot be converted to a `ByteBuf` fails
the exchange instead of sending nothing.
+
+With `sync=false` the producer continues routing as soon as the request is
written, and ignores any message the remote
+server sends back.
+
==== Using Multiple Codecs
In certain cases, it may be necessary to add chains of encoders and
diff --git a/components/camel-netty/src/main/docs/netty-component.adoc
b/components/camel-netty/src/main/docs/netty-component.adoc
index 5ce184a15f31..8a8a558565ba 100644
--- a/components/camel-netty/src/main/docs/netty-component.adoc
+++ b/components/camel-netty/src/main/docs/netty-component.adoc
@@ -160,6 +160,16 @@ exception thrown by the route, so the reply carries
neither its class name nor i
client receives the line `java.lang.Exception: Exchange processing failed`.
Set `muteException=false` to write the
exception of the route, as in earlier versions.
+The default codec (no `textline`, no custom `encoders` or `decoders`) only
writes text. The consumer therefore converts
+a reply that is not a `String` (or a `ByteBuf`), such as bytes, a stream, a
file, a `Map` or the exception of a failed
+exchange, to a `String` before writing it, as it does with the textline codec.
If no reply can be written, the
+consumer closes the TCP channel, so the client does not wait for a reply that
never comes.
+
+With `useByteBuf=true` a body that cannot be converted to a `ByteBuf` fails
the exchange instead of sending nothing.
+
+With `sync=false` the producer continues routing as soon as the request is
written, and ignores any message the remote
+server sends back.
+
==== Using Multiple Codecs
In certain cases, it may be necessary to add chains of encoders and
diff --git
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/NettyConfiguration.java
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/NettyConfiguration.java
index c08b36a4e1b0..0533043928d9 100644
---
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/NettyConfiguration.java
+++
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/NettyConfiguration.java
@@ -52,6 +52,7 @@ public class NettyConfiguration extends
NettyServerBootstrapConfiguration implem
private transient List<ChannelHandler> encodersList = new ArrayList<>();
private transient List<ChannelHandler> decodersList = new ArrayList<>();
+ private transient boolean defaultStringCodec;
@UriParam(label = "producer",
description = "Allows to use a timeout for the Netty producer
when calling a remote server. By default no timeout is in use. The"
@@ -343,6 +344,7 @@ public class NettyConfiguration extends
NettyServerBootstrapConfiguration implem
Charset charset = getEncoding() != null ?
Charset.forName(getEncoding()) : CharsetUtil.UTF_8;
encodersList.add(ChannelHandlerFactories.newStringEncoder(charset, protocol));
decodersList.add(ChannelHandlerFactories.newStringDecoder(charset, protocol));
+ defaultStringCodec = true;
}
if ("udp".equalsIgnoreCase(protocol)) {
decodersList.add(ChannelHandlerFactories.newDatagramPacketDecoder());
@@ -493,6 +495,14 @@ public class NettyConfiguration extends
NettyServerBootstrapConfiguration implem
return decoders;
}
+ /**
+ * Whether the default codec, which only writes a String, is in use: no
textline, no custom encoders or decoders,
+ * and allowDefaultCodec enabled.
+ */
+ public boolean isDefaultStringCodec() {
+ return defaultStringCodec;
+ }
+
public List<ChannelHandler> getEncodersAsList() {
return encodersList;
}
diff --git
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/NettyPayloadHelper.java
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/NettyPayloadHelper.java
index d09c5d8ae4d5..c5211e56f4d0 100644
---
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/NettyPayloadHelper.java
+++
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/NettyPayloadHelper.java
@@ -21,6 +21,8 @@ import java.net.InetSocketAddress;
import io.netty.buffer.ByteBuf;
import io.netty.channel.AddressedEnvelope;
import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.Message;
import org.apache.camel.support.DefaultExchangeHolder;
/**
@@ -36,28 +38,31 @@ public final class NettyPayloadHelper {
//Helper class
}
- public static Object getIn(NettyEndpoint endpoint, Exchange exchange) {
+ public static Object getIn(NettyEndpoint endpoint, Exchange exchange)
throws InvalidPayloadException {
if (endpoint.getConfiguration().isTransferExchange()) {
// we should transfer the entire exchange over the wire (includes
in/out)
return DefaultExchangeHolder.marshal(exchange, true,
endpoint.getConfiguration().isAllowSerializedHeaders());
} else {
- if (endpoint.getConfiguration().isUseByteBuf()) {
- // Just leverage the type converter
- return exchange.getIn().getBody(ByteBuf.class);
- } else {
- // normal transfer using the body only
- return exchange.getIn().getBody();
- }
+ return getBody(endpoint, exchange.getIn());
}
}
- public static Object getOut(NettyEndpoint endpoint, Exchange exchange) {
+ public static Object getOut(NettyEndpoint endpoint, Exchange exchange)
throws InvalidPayloadException {
if (endpoint.getConfiguration().isTransferExchange()) {
// we should transfer the entire exchange over the wire (includes
in/out)
return DefaultExchangeHolder.marshal(exchange);
+ } else {
+ return getBody(endpoint, exchange.getOut());
+ }
+ }
+
+ private static Object getBody(NettyEndpoint endpoint, Message message)
throws InvalidPayloadException {
+ if (endpoint.getConfiguration().isUseByteBuf() && message.getBody() !=
null) {
+ // Just leverage the type converter, and fail if the body cannot
be converted instead of sending nothing
+ return message.getMandatoryBody(ByteBuf.class);
} else {
// normal transfer using the body only
- return exchange.getOut().getBody();
+ return message.getBody();
}
}
diff --git
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/handlers/ClientChannelHandler.java
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/handlers/ClientChannelHandler.java
index 2205e096f57e..287aa046cfe0 100644
---
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/handlers/ClientChannelHandler.java
+++
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/handlers/ClientChannelHandler.java
@@ -156,6 +156,15 @@ public class ClientChannelHandler extends
SimpleChannelInboundHandler<Object> {
LOG.trace("Message received: {}", msg);
}
+ if (!producer.getConfiguration().isSync()) {
+ // the exchange continued routing as soon as the request was
written, so a message the remote server
+ // sends back must not be set on it (or on the next exchange that
uses this channel)
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Ignoring message received on channel: {} as the
producer is not sync", ctx.channel());
+ }
+ return;
+ }
+
ChannelHandler handler = ctx.pipeline().get("timeout");
if (handler != null) {
LOG.trace("Removing timeout channel as we received message");
diff --git
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/handlers/ServerChannelHandler.java
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/handlers/ServerChannelHandler.java
index ba78e535ad1e..54ba8f50caa2 100644
---
a/components/camel-netty/src/main/java/org/apache/camel/component/netty/handlers/ServerChannelHandler.java
+++
b/components/camel-netty/src/main/java/org/apache/camel/component/netty/handlers/ServerChannelHandler.java
@@ -18,12 +18,14 @@ package org.apache.camel.component.netty.handlers;
import java.net.SocketAddress;
+import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangePattern;
import org.apache.camel.ExchangePropertyKey;
+import org.apache.camel.component.netty.NettyConfiguration;
import org.apache.camel.component.netty.NettyConstants;
import org.apache.camel.component.netty.NettyConsumer;
import org.apache.camel.component.netty.NettyHelper;
@@ -179,7 +181,23 @@ public class ServerChannelHandler extends
SimpleChannelInboundHandler<Object> {
}
private void sendResponse(ChannelHandlerContext ctx, Exchange exchange)
throws Exception {
- Object body = getResponseBody(exchange);
+ Object body;
+ try {
+ body = getResponseBody(exchange);
+ if (body != null) {
+ body = convertResponseBody(exchange, body);
+ }
+ } catch (Exception e) {
+ if (consumer.getConfiguration().isTcp()) {
+ // no reply can be written, so close the channel, otherwise
the client waits for a reply forever
+ // (if not timing out)
+ if (LOG.isTraceEnabled()) {
+ LOG.trace("Closing channel as the reply cannot be written
at address: {}", ctx.channel().remoteAddress());
+ }
+ NettyHelper.close(ctx.channel());
+ }
+ throw e;
+ }
if (body == null) {
noReplyLogger.log("No payload to send as reply for exchange: " +
exchange);
@@ -192,12 +210,6 @@ public class ServerChannelHandler extends
SimpleChannelInboundHandler<Object> {
NettyHelper.close(ctx.channel());
}
} else {
- // if textline enabled then covert to a String which must be used
for textline
- if (consumer.getConfiguration().isTextline()) {
- body = NettyHelper.getTextlineBody(body, exchange,
consumer.getConfiguration().getDelimiter(),
- consumer.getConfiguration().isAutoAppendDelimiter());
- }
-
// we got a body to write
ChannelFutureListener listener =
createResponseFutureListener(consumer, exchange, ctx.channel().remoteAddress());
if (consumer.getConfiguration().isTcp()) {
@@ -210,6 +222,24 @@ public class ServerChannelHandler extends
SimpleChannelInboundHandler<Object> {
}
}
+ private Object convertResponseBody(Exchange exchange, Object body) throws
Exception {
+ NettyConfiguration configuration = consumer.getConfiguration();
+ // if textline enabled then covert to a String which must be used for
textline
+ if (configuration.isTextline()) {
+ return NettyHelper.getTextlineBody(body, exchange,
configuration.getDelimiter(),
+ configuration.isAutoAppendDelimiter());
+ }
+ // the default codec only writes a String (or a ByteBuf as-is), so
convert the reply to a String as textline does,
+ // otherwise the write fails and the client gets no reply. Only the
pipeline of camel-netty itself has the
+ // default codec, not one built by a custom ServerInitializerFactory.
+ if (configuration.isDefaultStringCodec() &&
configuration.getServerInitializerFactory() == null
+ && !configuration.isTransferExchange()
+ && !(body instanceof CharSequence) && !(body instanceof
ByteBuf)) {
+ return
exchange.getContext().getTypeConverter().mandatoryConvertTo(String.class,
exchange, body);
+ }
+ return body;
+ }
+
/**
* Gets the object we want to use as the response object for sending to
netty.
*
diff --git
a/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyDefaultCodecReplyTest.java
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyDefaultCodecReplyTest.java
new file mode 100644
index 000000000000..b2489e8d8a6b
--- /dev/null
+++
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyDefaultCodecReplyTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.netty;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * The default codec (no textline, no custom encoders) only writes a String: a
reply of another type, including the
+ * exception of a failed exchange, is converted to a String instead of failing
the write and leaving the client waiting.
+ */
+class NettyDefaultCodecReplyTest extends BaseNettyTest {
+
+ private static final String DETAIL =
"the-internal-detail-a-peer-must-not-see";
+
+ @RegisterExtension
+ AvailablePortFinder.Port unmutedPort = AvailablePortFinder.find();
+
+ @RegisterExtension
+ AvailablePortFinder.Port mapPort = AvailablePortFinder.find();
+
+ @RegisterExtension
+ AvailablePortFinder.Port bytesPort = AvailablePortFinder.find();
+
+ @Test
+ void aFailedExchangeRepliesTheMutedException() {
+ assertEquals("java.lang.Exception: Exchange processing failed",
request(port.getPort()));
+ }
+
+ @Test
+ void aFailedExchangeRepliesTheExceptionWhenNotMuted() {
+ assertEquals("java.lang.IllegalStateException: " + DETAIL,
request(unmutedPort.getPort()));
+ }
+
+ @Test
+ void aMapReplyIsConvertedToString() {
+ assertEquals("{greeting=Bye World}", request(mapPort.getPort()));
+ }
+
+ @Test
+ void aByteArrayReplyIsConvertedToString() {
+ assertEquals("Bye World", request(bytesPort.getPort()));
+ }
+
+ private String request(int port) {
+ // without the conversion the client gets no reply and fails with a
ReadTimeoutException
+ return template.requestBody("netty:tcp://localhost:" + port +
"?sync=true&requestTimeout=5000", "Hello World",
+ String.class);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("netty:tcp://localhost:{{port}}?sync=true")
+ .throwException(new IllegalStateException(DETAIL));
+
+
fromF("netty:tcp://localhost:%s?sync=true&muteException=false",
unmutedPort.getPort())
+ .throwException(new IllegalStateException(DETAIL));
+
+ fromF("netty:tcp://localhost:%s?sync=true", mapPort.getPort())
+ .process(exchange ->
exchange.getMessage().setBody(Map.of("greeting", "Bye World")));
+
+ fromF("netty:tcp://localhost:%s?sync=true",
bytesPort.getPort())
+ .process(exchange ->
exchange.getMessage().setBody("Bye World".getBytes(StandardCharsets.UTF_8)));
+ }
+ };
+ }
+}
diff --git
a/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyProducerSyncFalseReplyTest.java
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyProducerSyncFalseReplyTest.java
new file mode 100644
index 000000000000..b463a6cd0f2b
--- /dev/null
+++
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyProducerSyncFalseReplyTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.netty;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+
+/**
+ * With sync=false the producer continues routing as soon as the request is
written, so a reply that the remote server
+ * sends later must not be set on the exchange.
+ */
+class NettyProducerSyncFalseReplyTest extends BaseNettyTest {
+
+ @Test
+ void theReplyOfTheServerIsNotSetOnTheExchange() throws Exception {
+ getMockEndpoint("mock:server").expectedBodiesReceived("Hello World");
+ getMockEndpoint("mock:result").expectedBodiesReceived("Hello World");
+ getMockEndpoint("mock:result").setAssertPeriod(500);
+
+ template.sendBody("direct:start", "Hello World");
+
+ MockEndpoint.assertIsSatisfied(context);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start")
+ .to("netty:tcp://localhost:{{port}}?sync=false")
+ // continue on another thread so the netty I/O thread
is free to read the reply of the server
+ .threads(1)
+ // the body must stay the request, also after the
reply of the server has arrived
+ .process(exchange -> await().during(1,
TimeUnit.SECONDS).atMost(5, TimeUnit.SECONDS)
+ .until(() -> "Hello
World".equals(exchange.getMessage().getBody(String.class))))
+ .to("mock:result");
+
+ from("netty:tcp://localhost:{{port}}?sync=true")
+ .to("mock:server")
+ .setBody(constant("Bye World"));
+ }
+ };
+ }
+}
diff --git
a/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyUseByteBufTest.java
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyUseByteBufTest.java
new file mode 100644
index 000000000000..0e1c63fd6fe6
--- /dev/null
+++
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyUseByteBufTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.netty;
+
+import org.apache.camel.CamelExchangeException;
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * With useByteBuf=true a body that cannot be converted to a ByteBuf fails the
exchange instead of sending nothing.
+ */
+class NettyUseByteBufTest extends BaseNettyTest {
+
+ @RegisterExtension
+ AvailablePortFinder.Port replyPort = AvailablePortFinder.find();
+
+ @Test
+ void aStringBodyIsSent() throws Exception {
+ getMockEndpoint("mock:result").expectedBodiesReceived("Hello World");
+
+
template.sendBody("netty:tcp://localhost:{{port}}?sync=false&useByteBuf=true",
"Hello World");
+
+ MockEndpoint.assertIsSatisfied(context);
+ }
+
+ @Test
+ void aBodyThatCannotBeConvertedFailsTheExchange() {
+ CamelExecutionException e = assertThrows(CamelExecutionException.class,
+ () ->
template.sendBody("netty:tcp://localhost:{{port}}?sync=false&useByteBuf=true",
new Order()));
+ assertInstanceOf(InvalidPayloadException.class, e.getCause());
+ }
+
+ @Test
+ void aReplyThatCannotBeConvertedClosesTheChannel() {
+ // the client is not left waiting until its request timeout: the
consumer closes the channel
+ CamelExecutionException e = assertThrows(CamelExecutionException.class,
+ () -> template.requestBody("netty:tcp://localhost:" +
replyPort.getPort() + "?sync=true&requestTimeout=5000",
+ "Hello World"));
+ CamelExchangeException cause =
assertInstanceOf(CamelExchangeException.class, e.getCause());
+ assertTrue(cause.getMessage().startsWith("No response received from
remote server"), cause.getMessage());
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("netty:tcp://localhost:{{port}}?sync=false")
+ .to("mock:result");
+
+
fromF("netty:tcp://localhost:%s?sync=true&useByteBuf=true&disconnectOnNoReply=false",
replyPort.getPort())
+ .process(exchange -> exchange.getMessage().setBody(new
Order()));
+ }
+ };
+ }
+
+ private static final class Order {
+ @Override
+ public String toString() {
+ return "Order";
+ }
+ }
+}
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 f70dd6e55b38..a5bf98a4b023 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
@@ -1750,6 +1750,22 @@ exception of the route. With an encoder that accepts any
object, the serialised
message, cause or stack trace of the exception of the route. Set
`muteException=false` to write the exception as before.
`transferExchange=true` is not affected.
+=== camel-netty - replies with the default codec, useByteBuf and sync=false
+
+With the default codec (no `textline`, no custom `encoders` or `decoders`) a
consumer with `sync=true` now converts a
+reply that is not a `String` (or a `ByteBuf`) to a `String` before writing it,
as with `textline=true`. Before, such a
+reply, including the exception of a failed exchange, could not be written and
the client got no reply. A failed exchange
+now replies `java.lang.Exception: Exchange processing failed` (see
`muteException` above). When the reply cannot be
+written, the consumer closes the TCP channel instead of leaving the client
waiting.
+
+With `useByteBuf=true` a body that cannot be converted to a `ByteBuf` (such as
a `File`, a `Map` or a POJO) now fails
+the exchange with an `InvalidPayloadException`, where nothing was sent before.
A consumer with `useByteBuf=true` now
+also converts a reply set as the out message to a `ByteBuf`.
`NettyPayloadHelper.getIn` and `getOut` declare
+`InvalidPayloadException`.
+
+With `sync=false` the producer now ignores a message the remote server sends
back. Before, that reply was set on the
+exchange after it had already continued routing.
+
=== camel-telegram - webhook requests are verified with a secret token
In webhook mode, Camel now registers the webhook with a secret token, which
Telegram sends in the