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 45a165444069 CAMEL-26389: camel-core - RedeliveryErrorHandler: keep a 
copy of the IN message for redelivery instead of the whole Exchange (#28648)
45a165444069 is described below

commit 45a165444069a225a0d91ac870588b801996fa7f
Author: Adriano Machado <[email protected]>
AuthorDate: Thu Oct 29 01:38:40 2026 -0400

    CAMEL-26389: camel-core - RedeliveryErrorHandler: keep a copy of the IN 
message for redelivery instead of the whole Exchange (#28648)
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../errorhandler/RedeliveryErrorHandler.java       |  20 ++--
 .../RedeliveryErrorHandlerCopyInMessageTest.java   | 103 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  12 +++
 3 files changed, 127 insertions(+), 8 deletions(-)

diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java
index 99e655477a45..903cf73f14af 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java
@@ -285,9 +285,12 @@ public abstract class RedeliveryErrorHandler extends 
ErrorHandlerSupport
     /**
      * Performs a defensive copy of the exchange if needed
      *
-     * @param  exchange the exchange
-     * @return          the defensive copy, or <tt>null</tt> if not needed 
(redelivery is not enabled).
+     * @param      exchange the exchange
+     * @return              the defensive copy, or <tt>null</tt> if not needed 
(redelivery is not enabled).
+     * @deprecated          since 4.24.0. No longer called: redelivery only 
restores the IN message, so the error
+     *                      handler keeps a copy of the IN message instead of 
the whole exchange.
      */
+    @Deprecated(since = "4.24.0")
     protected Exchange defensiveCopyExchangeIfNeeded(Exchange exchange) {
         // only do a defensive copy if redelivery is enabled
         if (redeliveryEnabled) {
@@ -1045,7 +1048,7 @@ public abstract class RedeliveryErrorHandler extends 
ErrorHandlerSupport
      */
     protected class RedeliveryTask implements PooledExchangeTask {
         // state
-        private Exchange original;
+        private Message original;
         private Exchange exchange;
         private AsyncCallback callback;
         private int redeliveryCounter;
@@ -1073,9 +1076,10 @@ public abstract class RedeliveryErrorHandler extends 
ErrorHandlerSupport
         @Override
         public void prepare(Exchange exchange, AsyncCallback callback) {
             useErrorHandlerDefaults();
-            // do a defensive copy of the original Exchange, which is needed 
for redelivery so we can ensure the
-            // original Exchange is being redelivered, and not a mutated 
Exchange
-            this.original = redeliveryEnabled ? 
defensiveCopyExchangeIfNeeded(exchange) : null;
+            // do a defensive copy of the original IN message, which is needed 
for redelivery so we can ensure the
+            // original message is being redelivered, and not a mutated 
message. Only the IN message is restored on
+            // redelivery, and this runs for every node on the success path, 
so do not copy the whole Exchange
+            this.original = redeliveryEnabled ? exchange.getIn().copy() : null;
             this.exchange = exchange;
             this.callback = callback;
         }
@@ -1414,7 +1418,7 @@ public abstract class RedeliveryErrorHandler extends 
ErrorHandlerSupport
                                                 + ". Make sure you have 
configured the error handler properly.");
             }
             // there must be a defensive copy of the exchange
-            ObjectHelper.notNull(this.original, "Defensive copy of Exchange is 
null", RedeliveryErrorHandler.this);
+            ObjectHelper.notNull(this.original, "Defensive copy of IN message 
is null", RedeliveryErrorHandler.this);
 
             // okay we will give it another go so clear the exception so we 
can try again
             exchange.setException(null);
@@ -1423,7 +1427,7 @@ public abstract class RedeliveryErrorHandler extends 
ErrorHandlerSupport
             exchange.setRollbackOnly(false);
 
             // we are redelivering so copy from original back to exchange
-            exchange.getIn().copyFrom(this.original.getIn());
+            exchange.getIn().copyFrom(this.original);
             exchange.setOut(null);
             // reset cached streams so they can be read again
             MessageHelper.resetStreamCache(exchange.getIn());
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/RedeliveryErrorHandlerCopyInMessageTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/RedeliveryErrorHandlerCopyInMessageTest.java
new file mode 100644
index 000000000000..15882824efdb
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/RedeliveryErrorHandlerCopyInMessageTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.processor;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.SafeCopyProperty;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * The redelivery error handler keeps a copy of the IN message for each node, 
so it can redeliver the original message.
+ * It must not copy the whole exchange for that, as this happens for every 
node on the success path.
+ */
+public class RedeliveryErrorHandlerCopyInMessageTest extends 
ContextTestSupport {
+
+    private final AtomicInteger exchangeCopies = new AtomicInteger();
+    private final List<String> attempts = new ArrayList<>();
+
+    @Test
+    public void testSuccessPathDoesNotCopyExchange() {
+        Exchange out = template.send("direct:success", e -> 
e.getIn().setBody("Hello World"));
+
+        assertNull(out.getException());
+        assertEquals("Hello World", out.getMessage().getBody());
+        // every copy of the exchange calls safeCopy on this property, so 0 
means the nodes after it did not copy
+        assertEquals(0, exchangeCopies.get());
+    }
+
+    @Test
+    public void testRedeliveryRestoresOriginalInMessage() {
+        Exchange out = template.send("direct:redeliver", e -> {
+            e.getIn().setBody("Hello World");
+            e.getIn().setHeader("foo", "original");
+        });
+
+        assertNull(out.getException());
+        // each attempt sees the original body and header, not what the failed 
attempt changed
+        assertEquals(List.of("Hello World/original", "Hello World/original", 
"Hello World/original"), attempts);
+        assertEquals("Bye World", out.getMessage().getBody());
+        // exchange properties are not part of the message, so they are not 
restored on redelivery
+        assertEquals(2, out.getProperty("failedAttempts", Integer.class));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
errorHandler(defaultErrorHandler().maximumRedeliveries(3).redeliveryDelay(0));
+
+                from("direct:success")
+                        .process(e -> 
e.getExchangeExtension().setSafeCopyProperty("counter", new CountingProperty()))
+                        .setHeader("step", constant(1))
+                        .setHeader("step", constant(2))
+                        .process(e -> e.getIn().setHeader("step", 3));
+
+                from("direct:redeliver")
+                        .process(e -> {
+                            attempts.add(e.getIn().getBody() + "/" + 
e.getIn().getHeader("foo"));
+                            e.getIn().setBody("Changed");
+                            e.getIn().setHeader("foo", "changed");
+                            if (attempts.size() < 3) {
+                                int failed = e.getProperty("failedAttempts", 
0, Integer.class);
+                                e.setProperty("failedAttempts", failed + 1);
+                                throw new IllegalArgumentException("Forced 
attempt " + attempts.size());
+                            }
+                            e.getIn().setBody("Bye World");
+                        });
+            }
+        };
+    }
+
+    private final class CountingProperty implements SafeCopyProperty {
+
+        @Override
+        public SafeCopyProperty safeCopy() {
+            exchangeCopies.incrementAndGet();
+            return this;
+        }
+    }
+}
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 3f7d5727bcc6..b761304b66bb 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
@@ -100,6 +100,18 @@ in its model, so clearing them on the route saved little 
memory at the cost of m
 Applications that called `Route.clearRouteModel()` directly can remove the 
call. Implementations of the `Route` interface
 that override `clearRouteModel()` can remove the override.
 
+=== camel-core - RedeliveryErrorHandler.defensiveCopyExchangeIfNeeded is 
deprecated
+
+When redelivery is enabled, the error handler (`DefaultErrorHandler`, 
`DeadLetterChannel` and the transaction error
+handlers) keeps a copy of the message before each step of the route, so a 
redelivery starts again from the original
+message. It copied the whole exchange for that, at every step, also when the 
step succeeded, but a redelivery only ever
+restored the IN message from it. It now keeps a copy of the IN message only. 
Routes behave as before: exchange
+properties and variables were not restored on redelivery, and still are not.
+
+The protected `RedeliveryErrorHandler.defensiveCopyExchangeIfNeeded(Exchange)` 
method is deprecated and no longer called.
+A custom error handler that overrides it to change what is redelivered must 
change the message before the redelivery
+instead, for example with `onRedelivery`.
+
 === camel-core - the documented default of skipBindingOnErrorCode is true
 
 The `skipBindingOnErrorCode` option of the Rest DSL (on `restConfiguration`, 
`rest`, its verbs and `restBinding`) is now

Reply via email to