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 f2776cd8a05b CAMEL-26587: camel-netty - check transferExchange before
processing transferred exchange objects (#29165)
f2776cd8a05b is described below
commit f2776cd8a05b3119a661d2bddf2e1c40d1000a00
Author: Andrea Cosentino <[email protected]>
AuthorDate: Wed Nov 4 19:53:52 2026 +0100
CAMEL-26587: camel-netty - check transferExchange before processing
transferred exchange objects (#29165)
camel-netty only rebuilds an exchange from a DefaultExchangeHolder when the
endpoint has transferExchange=true, on both the TCP and the AddressedEnvelope
paths, like camel-mina (CAMEL-24509). The two-argument
NettyPayloadHelper.setIn/setOut(Exchange, Object) are deprecated and no longer
unmarshal a holder. Upgrade guide updated.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
---
.../camel/component/netty/NettyPayloadHelper.java | 75 +++++++++--
.../netty/handlers/ClientChannelHandler.java | 4 +-
.../netty/handlers/ServerChannelHandler.java | 2 +-
.../netty/NettyTransferExchangeDisabledTest.java | 149 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 8 ++
5 files changed, 225 insertions(+), 13 deletions(-)
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 8585789e0e8d..35495b4d28e4 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
@@ -108,15 +108,24 @@ public final class NettyPayloadHelper {
}
}
- public static void setIn(Exchange exchange, Object payload) {
- if (payload instanceof DefaultExchangeHolder defaultExchangeHolder) {
- unmarshal(exchange, defaultExchangeHolder);
+ /**
+ * Sets the inbound message of the exchange from a decoded Netty payload.
A {@link DefaultExchangeHolder} rebuilds
+ * the exchange only when the endpoint has {@code transferExchange=true};
otherwise it is set as the message body
+ * like any other payload. An {@link AddressedEnvelope} (UDP) is unwrapped
and its sender address is recorded.
+ *
+ * @param endpoint the endpoint that received the payload, whose
configuration decides on the exchange transfer
+ * @param exchange the exchange to set the message on
+ * @param payload the decoded payload
+ */
+ public static void setIn(NettyEndpoint endpoint, Exchange exchange, Object
payload) {
+ if (isExchangeTransfer(endpoint, payload)) {
+ unmarshal(exchange, (DefaultExchangeHolder) payload);
} else if (payload instanceof AddressedEnvelope) {
@SuppressWarnings("unchecked")
AddressedEnvelope<Object, InetSocketAddress> dp =
(AddressedEnvelope<Object, InetSocketAddress>) payload;
// need to check if the content is ExchangeHolder
- if (dp.content() instanceof DefaultExchangeHolder
defaultExchangeHolder) {
- unmarshal(exchange, defaultExchangeHolder);
+ if (isExchangeTransfer(endpoint, dp.content())) {
+ unmarshal(exchange, (DefaultExchangeHolder) dp.content());
} else {
// need to take out the payload here
exchange.getIn().setBody(dp.content());
@@ -131,15 +140,23 @@ public final class NettyPayloadHelper {
}
}
- public static void setOut(Exchange exchange, Object payload) {
- if (payload instanceof DefaultExchangeHolder defaultExchangeHolder) {
- unmarshal(exchange, defaultExchangeHolder);
+ /**
+ * Sets the outbound message of the exchange from a decoded Netty payload,
as {@link #setIn} does for the inbound
+ * message; a payload that is a plain body keeps the headers of the
inbound message.
+ *
+ * @param endpoint the endpoint that received the payload, whose
configuration decides on the exchange transfer
+ * @param exchange the exchange to set the message on
+ * @param payload the decoded payload
+ */
+ public static void setOut(NettyEndpoint endpoint, Exchange exchange,
Object payload) {
+ if (isExchangeTransfer(endpoint, payload)) {
+ unmarshal(exchange, (DefaultExchangeHolder) payload);
} else if (payload instanceof AddressedEnvelope) {
@SuppressWarnings("unchecked")
AddressedEnvelope<Object, InetSocketAddress> dp =
(AddressedEnvelope<Object, InetSocketAddress>) payload;
// need to check if the content is ExchangeHolder
- if (dp.content() instanceof DefaultExchangeHolder
defaultExchangeHolder) {
- unmarshal(exchange, defaultExchangeHolder);
+ if (isExchangeTransfer(endpoint, dp.content())) {
+ unmarshal(exchange, (DefaultExchangeHolder) dp.content());
} else {
// need to take out the payload here
exchange.getOut().setBody(dp.content());
@@ -153,6 +170,44 @@ public final class NettyPayloadHelper {
}
}
+ /**
+ * A decoded {@link DefaultExchangeHolder} rebuilds the entire Exchange
(id, body, headers, out message, properties
+ * and variables), so it is only unmarshalled when the endpoint has
explicitly enabled exchange transfer via
+ * {@code transferExchange=true}. Otherwise the payload is treated as a
normal message body.
+ */
+ private static boolean isExchangeTransfer(NettyEndpoint endpoint, Object
payload) {
+ return payload instanceof DefaultExchangeHolder
+ && endpoint != null &&
endpoint.getConfiguration().isTransferExchange();
+ }
+
+ /**
+ * @deprecated use {@link #setIn(NettyEndpoint, Exchange,
Object)}. This overload has no endpoint and so
+ * cannot check {@code transferExchange}: since
4.24.0 it never unmarshals a
+ * {@link DefaultExchangeHolder} and sets any
payload, a holder included, as the message body.
+ * A caller that relied on the unconditional
unmarshalling of earlier versions has to pass its
+ * endpoint to the three-argument overload.
+ * @param exchange the exchange
+ * @param payload the decoded payload
+ */
+ @Deprecated(since = "4.24.0", forRemoval = true)
+ public static void setIn(Exchange exchange, Object payload) {
+ setIn(null, exchange, payload);
+ }
+
+ /**
+ * @deprecated use {@link #setOut(NettyEndpoint, Exchange,
Object)}. This overload has no endpoint and so
+ * cannot check {@code transferExchange}: since
4.24.0 it never unmarshals a
+ * {@link DefaultExchangeHolder} and sets any
payload, a holder included, as the message body.
+ * A caller that relied on the unconditional
unmarshalling of earlier versions has to pass its
+ * endpoint to the three-argument overload.
+ * @param exchange the exchange
+ * @param payload the decoded payload
+ */
+ @Deprecated(since = "4.24.0", forRemoval = true)
+ public static void setOut(Exchange exchange, Object payload) {
+ setOut(null, exchange, payload);
+ }
+
/**
* The headers and exchange properties that describe the Netty connection
of this side.
*/
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 287aa046cfe0..b9cdc6735d12 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
@@ -266,10 +266,10 @@ public class ClientChannelHandler extends
SimpleChannelInboundHandler<Object> {
// set the result on either IN or OUT on the original exchange
depending on its pattern
if (ExchangeHelper.isOutCapable(exchange)) {
- NettyPayloadHelper.setOut(exchange, body);
+ NettyPayloadHelper.setOut(producer.getEndpoint(), exchange, body);
return exchange.getOut();
} else {
- NettyPayloadHelper.setIn(exchange, body);
+ NettyPayloadHelper.setIn(producer.getEndpoint(), exchange, body);
return exchange.getIn();
}
}
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 54ba8f50caa2..d80b0980b257 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
@@ -132,7 +132,7 @@ public class ServerChannelHandler extends
SimpleChannelInboundHandler<Object> {
// must be prototype scoped (not pooled) so we create the exchange via
endpoint
Exchange exchange = consumer.createExchange(false);
consumer.getEndpoint().updateMessageHeader(exchange.getIn(), ctx);
- NettyPayloadHelper.setIn(exchange, message);
+ NettyPayloadHelper.setIn(consumer.getEndpoint(), exchange, message);
return exchange;
}
diff --git
a/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyTransferExchangeDisabledTest.java
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyTransferExchangeDisabledTest.java
new file mode 100644
index 000000000000..829fff44be15
--- /dev/null
+++
b/components/camel-netty/src/test/java/org/apache/camel/component/netty/NettyTransferExchangeDisabledTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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 io.netty.channel.ChannelHandler;
+import io.netty.handler.codec.serialization.ClassResolvers;
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.component.netty.codec.ObjectDecoder;
+import org.apache.camel.component.netty.codec.ObjectEncoder;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.support.DefaultExchangeHolder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class NettyTransferExchangeDisabledTest extends BaseNettyTest {
+
+ @BindToRegistry("encoder")
+ public ChannelHandler getEncoder() {
+ return new ShareableChannelHandlerFactory(new ObjectEncoder());
+ }
+
+ @BindToRegistry("decoder")
+ public ChannelHandler getDecoder() {
+ return new DefaultChannelHandlerFactory() {
+ @Override
+ public ChannelHandler newChannelHandler() {
+ return new
ObjectDecoder(ClassResolvers.weakCachingResolver(null));
+ }
+ };
+ }
+
+ @Test
+ public void testTransferExchangeDisabledTreatsHolderAsBody() throws
Exception {
+ MockEndpoint mock = getMockEndpoint("mock:result");
+ mock.expectedMessageCount(1);
+
+ Exchange sourceExchange = new DefaultExchange(context);
+ sourceExchange.getIn().setBody("Inner Payload");
+ sourceExchange.getIn().setHeader("CustomHeader", "HeaderValue");
+ sourceExchange.setProperty("CustomProperty", "PropertyValue");
+
+ DefaultExchangeHolder holder =
DefaultExchangeHolder.marshal(sourceExchange, true, true);
+
+ Endpoint endpoint
+ =
context.getEndpoint("netty:tcp://localhost:{{port}}?sync=false&encoders=#encoder&decoders=#decoder");
+ Producer producer = endpoint.createProducer();
+ producer.start();
+ try {
+ Exchange sendExchange = endpoint.createExchange();
+ sendExchange.getIn().setBody(holder);
+ producer.process(sendExchange);
+ } finally {
+ producer.stop();
+ }
+
+ mock.assertIsSatisfied();
+
+ Exchange received = mock.getReceivedExchanges().get(0);
+ assertNotNull(received);
+
+ // Holder must remain the body and NOT be unmarshalled into the
exchange
+ assertTrue(received.getIn().getBody() instanceof DefaultExchangeHolder,
+ "Body should remain a DefaultExchangeHolder when
transferExchange=false");
+ assertNull(received.getIn().getHeader("CustomHeader"),
+ "Headers from DefaultExchangeHolder must not be unmarshalled
when transferExchange=false");
+ assertNull(received.getProperty("CustomProperty"),
+ "Properties from DefaultExchangeHolder must not be
unmarshalled when transferExchange=false");
+
assertNotNull(received.getIn().getHeader(NettyConstants.NETTY_REMOTE_ADDRESS),
+ "Connection headers must be preserved");
+ }
+
+ @Test
+ public void testHelperGating() {
+ NettyEndpoint transferOn =
context.getEndpoint("netty:tcp://localhost:1111?transferExchange=true",
NettyEndpoint.class);
+ NettyEndpoint transferOff
+ =
context.getEndpoint("netty:tcp://localhost:2222?transferExchange=false",
NettyEndpoint.class);
+
+ Exchange source = new DefaultExchange(context);
+ source.getIn().setBody("Inner Content");
+ source.getIn().setHeader("Key", "Value");
+ source.setProperty("Prop", "Val");
+ DefaultExchangeHolder holder = DefaultExchangeHolder.marshal(source,
true, true);
+
+ // 1. With transferExchange = true
+ Exchange targetOn = new DefaultExchange(context);
+ NettyPayloadHelper.setIn(transferOn, targetOn, holder);
+ assertEquals("Inner Content", targetOn.getIn().getBody());
+ assertEquals("Value", targetOn.getIn().getHeader("Key"));
+ assertEquals("Val", targetOn.getProperty("Prop"));
+
+ // 2. With transferExchange = false
+ Exchange targetOff = new DefaultExchange(context);
+ NettyPayloadHelper.setIn(transferOff, targetOff, holder);
+ assertSame(holder, targetOff.getIn().getBody());
+ assertNull(targetOff.getIn().getHeader("Key"));
+ assertNull(targetOff.getProperty("Prop"));
+
+ // 3. With null endpoint (deprecated overload)
+ Exchange targetNull = new DefaultExchange(context);
+ NettyPayloadHelper.setIn(targetNull, holder);
+ assertSame(holder, targetNull.getIn().getBody());
+ assertNull(targetNull.getIn().getHeader("Key"));
+ assertNull(targetNull.getProperty("Prop"));
+
+ // 4. setOut with transferExchange = false
+ Exchange targetOutOff = new DefaultExchange(context);
+ targetOutOff.getIn().setHeader("ExistingHeader", "ExistingVal");
+ NettyPayloadHelper.setOut(transferOff, targetOutOff, holder);
+ assertSame(holder, targetOutOff.getMessage().getBody());
+ assertNull(targetOutOff.getMessage().getHeader("Key"));
+ assertEquals("ExistingVal",
targetOutOff.getMessage().getHeader("ExistingHeader"));
+ assertNull(targetOutOff.getProperty("Prop"));
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("netty:tcp://localhost:{{port}}?sync=false&encoders=#encoder&decoders=#decoder")
+ .to("mock:result");
+ }
+ };
+ }
+}
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 fc8c275c33b7..9e44e048f532 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
@@ -2400,6 +2400,14 @@ a header in the transferred exchange is no longer used,
also when the receiving
the `CamelNettySSLClientCert*` headers on a connection without a client
certificate). This also applies to the reply
the producer receives. Send any such value of the sender under a header name
of your own.
+=== camel-netty - transferExchange requires explicit endpoint configuration
+
+`NettyPayloadHelper` now checks `transferExchange=true` on the endpoint
configuration before reconstructing exchange data from a
`DefaultExchangeHolder` payload.
+
+Before, a receiver with an object decoder silently reconstructed the exchange
even when `transferExchange=false`. Now, when `transferExchange` is disabled
(the default), the incoming `DefaultExchangeHolder` is left as an opaque body
(both on the consumer side and for the producer's reply via `setOut`).
+
+In addition, the legacy two-argument methods
`NettyPayloadHelper.setIn(Exchange, Object)` and `setOut(Exchange, Object)` are
deprecated and no longer unmarshal `DefaultExchangeHolder` payloads. Callers
should migrate to the three-argument overloads passing the `NettyEndpoint`.
+
=== camel-netty-http - a response that cannot be built
When no HTTP response can be built from the reply of the route (for example
the body cannot be converted), the consumer