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 0a9a82c0b88e CAMEL-26857: camel-core - a doCatch marks the redelivery 
of the called route it caught as over, so a later error handler gets its own 
redeliveries (#29783)
0a9a82c0b88e is described below

commit 0a9a82c0b88e7e78a587a6ab27821358126fb056
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Nov 13 15:02:12 2026 +0530

    CAMEL-26857: camel-core - a doCatch marks the redelivery of the called 
route it caught as over, so a later error handler gets its own redeliveries 
(#29783)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/processor/CatchProcessor.java |   6 +
 .../errorhandler/RedeliveryErrorHandler.java       |  32 ++--
 .../camel/processor/DoCatchRedeliveryOverTest.java | 175 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   5 +
 4 files changed, 203 insertions(+), 15 deletions(-)

diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/CatchProcessor.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/CatchProcessor.java
index 6bce93ffa83c..e4e52953e992 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/CatchProcessor.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/CatchProcessor.java
@@ -31,6 +31,7 @@ import org.apache.camel.Predicate;
 import org.apache.camel.Processor;
 import org.apache.camel.RollbackExchangeException;
 import org.apache.camel.Traceable;
+import org.apache.camel.processor.errorhandler.RedeliveryErrorHandler;
 import org.apache.camel.spi.IdAware;
 import org.apache.camel.spi.InterceptableProcessor;
 import org.apache.camel.spi.RouteIdAware;
@@ -191,6 +192,11 @@ public class CatchProcessor extends 
BaseDelegateProcessorSupport
         if (ext.isFailureHandled() || ext.isErrorHandlerHandledSet()) {
             ext.setFailureHandled(false);
             ext.setErrorHandlerHandled(null);
+            // that route may have been redelivered before it failed, and left 
its redelivery headers on the message:
+            // they stay readable in the doCatch block, but that redelivery is 
over, so a later error handler does not
+            // count on from them (CAMEL-26857), as when the doTry failed in 
this route (no redelivery runs in a doTry).
+            // Only here: when the doTry failed in this route, a header the 
exchange brought into it is left as it was
+            RedeliveryErrorHandler.markRedeliveryOver(exchange);
         }
 
         if (LOG.isDebugEnabled()) {
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 fed82dafc252..3842cedca2f8 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
@@ -444,6 +444,21 @@ public abstract class RedeliveryErrorHandler extends 
ErrorHandlerSupport
         }
     }
 
+    /**
+     * Marks the redelivery headers on the message as those of a redelivery 
that is over: they stay, so the next steps
+     * can see that the message was redelivered, but a later failure does not 
count on from them. The redelivery task
+     * calls it when a redelivered step succeeded (CAMEL-26734), and a doCatch 
when it takes over the failure of a route
+     * that the doTry called and whose error handler redelivered it 
(CAMEL-26857).
+     * <p/>
+     * Internal API, for the processors of Camel.
+     */
+    public static void markRedeliveryOver(Exchange exchange) {
+        Integer counter = 
exchange.getIn().getHeader(Exchange.REDELIVERY_COUNTER, Integer.class);
+        if (counter != null) {
+            exchange.setProperty(REDELIVERY_OVER, counter);
+        }
+    }
+
     /**
      * Evaluates a predicate of the error handler (such as handled or 
continued). A predicate that fails (throws an
      * exception) is regarded as <tt>false</tt>, as the error handler must 
still handle the exchange (such as the dead
@@ -1658,7 +1673,8 @@ public abstract class RedeliveryErrorHandler extends 
ErrorHandlerSupport
                 // and it has not been handled by the error processor
                 if (isDone(exchange)) {
                     if (exchange.getException() == null) {
-                        markRedeliveryOver();
+                        // the redelivered step succeeded, so its redelivery 
is over (CAMEL-26734)
+                        RedeliveryErrorHandler.markRedeliveryOver(exchange);
                     }
                     AsyncCallback cb = callback;
                     taskFactory.release(this);
@@ -1670,20 +1686,6 @@ public abstract class RedeliveryErrorHandler extends 
ErrorHandlerSupport
             });
         }
 
-        /**
-         * The redelivered step succeeded, so its redelivery is over. The 
redelivery headers stay, so the next steps can
-         * see that the message was redelivered, but they are marked as over: 
otherwise a later failure, of another step
-         * of the route or of a route that gets the exchange (or a copy of it, 
such as over seda) next, would count on
-         * from them (incrementRedeliveryCounter respects a higher header, for 
a nested error handler that failed) and
-         * get fewer redeliveries, or none (CAMEL-26734).
-         */
-        private void markRedeliveryOver() {
-            Integer counter = 
exchange.getIn().getHeader(Exchange.REDELIVERY_COUNTER, Integer.class);
-            if (counter != null) {
-                exchange.setProperty(REDELIVERY_OVER, counter);
-            }
-        }
-
         protected void prepareExchangeForContinue(Exchange exchange, boolean 
isDeadLetterChannel) {
             Exception caught = exchange.getException();
             if (caught != null) {
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/DoCatchRedeliveryOverTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/DoCatchRedeliveryOverTest.java
new file mode 100644
index 000000000000..0d8602e8e932
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/DoCatchRedeliveryOverTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.TryDefinition;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * CAMEL-26857: after a doCatch caught the failure of a route that the doTry 
called and that was redelivered before it
+ * failed, a later error handler gets its own redeliveries, as when the doTry 
failed in the route itself. The redelivery
+ * headers of the called route stay readable in the doCatch block. Each case 
runs twice: with the doTry calling the
+ * failing route ("viaCallee") and with the doTry failing in the route 
("inRoute", the control), with the same expected
+ * outcome.
+ */
+class DoCatchRedeliveryOverTest extends ContextTestSupport {
+
+    private final Map<String, AtomicInteger> calls = new ConcurrentHashMap<>();
+
+    @ParameterizedTest
+    @ValueSource(strings = { "viaCallee", "inRoute" })
+    void laterFailureInTheCallingRouteGetsItsOwnRedeliveries(String variant) 
throws Exception {
+        MockEndpoint catchMock = getMockEndpoint("mock:catch");
+        catchMock.expectedMessageCount(1);
+        getMockEndpoint("mock:dead").expectedMessageCount(1);
+
+        template.sendBody("direct:laterFailure-" + variant, "Hello");
+
+        MockEndpoint.assertIsSatisfied(context);
+        // the step after the doTry: the first call and the 3 redeliveries of 
the calling route's error handler
+        assertEquals(4, calls("later-" + variant));
+        assertCatchSawTheRedeliveriesOfTheCallee(catchMock, variant);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "viaCallee", "inRoute" })
+    void routeCalledFromTheDoCatchGetsItsOwnRedeliveries(String variant) 
throws Exception {
+        MockEndpoint catchMock = getMockEndpoint("mock:catch");
+        catchMock.expectedMessageCount(1);
+        getMockEndpoint("mock:recovered").expectedMessageCount(1);
+
+        template.sendBody("direct:catchCalls-" + variant, "Hello");
+
+        MockEndpoint.assertIsSatisfied(context);
+        // sendBody returned: the exchange is done, and did not go to the dead 
letter channel
+        assertEquals(0, getMockEndpoint("mock:dead").getReceivedCounter());
+        // the route called from the doCatch fails 3 times and succeeds at its 
third redelivery
+        assertEquals(4, calls("recovering-" + variant));
+        assertCatchSawTheRedeliveriesOfTheCallee(catchMock, variant);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "viaCallee", "inRoute" })
+    void callingRouteGetsItsOwnRedeliveriesWhenTheDoCatchThrows(String 
variant) throws Exception {
+        getMockEndpoint("mock:dead").expectedMessageCount(1);
+
+        template.sendBody("direct:caller-" + variant, "Hello");
+
+        MockEndpoint.assertIsSatisfied(context);
+        // the route with the doTry fails (its doCatch throws), so the calling 
route redelivers its call: the doCatch
+        // runs at the first call and at each of the 3 redeliveries
+        assertEquals(4, calls("catchBlock-" + variant));
+    }
+
+    private void assertCatchSawTheRedeliveriesOfTheCallee(MockEndpoint 
catchMock, String variant) {
+        Exchange caught = catchMock.getReceivedExchanges().get(0);
+        if ("viaCallee".equals(variant)) {
+            // the called route: the first call and its 2 redeliveries
+            assertEquals(3, calls("callee-" + variant));
+            assertEquals(Boolean.TRUE, 
caught.getMessage().getHeader(Exchange.REDELIVERED));
+            assertEquals(2, 
caught.getMessage().getHeader(Exchange.REDELIVERY_COUNTER));
+        } else {
+            assertEquals(1, calls("callee-" + variant));
+            
assertNull(caught.getMessage().getHeader(Exchange.REDELIVERY_COUNTER));
+        }
+    }
+
+    private int calls(String name) {
+        return calls.computeIfAbsent(name, k -> new AtomicInteger()).get();
+    }
+
+    private int call(String name) {
+        return calls.computeIfAbsent(name, k -> new 
AtomicInteger()).incrementAndGet();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                // the error handler of the calling routes
+                
errorHandler(deadLetterChannel("mock:dead").maximumRedeliveries(3).redeliveryDelay(0));
+
+                for (String variant : new String[] { "viaCallee", "inRoute" }) 
{
+                    // a failure after the doTry, in the calling route
+                    failingTry(from("direct:laterFailure-" + variant), variant)
+                            .doCatch(IllegalArgumentException.class)
+                                .to("mock:catch")
+                            .end()
+                            .process(e -> {
+                                call("later-" + variant);
+                                throw new IllegalStateException("after the 
doTry");
+                            });
+
+                    // a route called from the doCatch block, which recovers 
at its third redelivery
+                    failingTry(from("direct:catchCalls-" + variant), variant)
+                            .doCatch(IllegalArgumentException.class)
+                                .to("mock:catch")
+                                .to("direct:recovering-" + variant)
+                            .end()
+                            .to("mock:recovered");
+                    from("direct:recovering-" + variant)
+                            .process(e -> {
+                                if (call("recovering-" + variant) < 4) {
+                                    throw new IllegalStateException("not yet");
+                                }
+                            });
+
+                    // a route that calls a route whose doCatch throws
+                    from("direct:caller-" + variant)
+                            .to("direct:catchThrows-" + variant);
+                    failingTry(from("direct:catchThrows-" + variant), variant)
+                            .doCatch(IllegalArgumentException.class)
+                                .process(e -> call("catchBlock-" + variant))
+                                .throwException(new IllegalStateException("in 
the doCatch"))
+                            .end();
+
+                    // the called route: redelivered twice by its own error 
handler, then fails unhandled
+                    from("direct:callee-" + variant)
+                            
.errorHandler(defaultErrorHandler().maximumRedeliveries(2).redeliveryDelay(0))
+                            .process(e -> {
+                                call("callee-" + variant);
+                                throw new IllegalArgumentException("callee 
fails");
+                            });
+                }
+            }
+
+            private TryDefinition failingTry(RouteDefinition route, String 
variant) {
+                if ("viaCallee".equals(variant)) {
+                    return route.doTry().to("direct:callee-" + variant);
+                }
+                return route.doTry().process(e -> {
+                    call("callee-" + variant);
+                    throw new IllegalArgumentException("fails in the route");
+                });
+            }
+        };
+    }
+}
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 e60fe669b26c..2ab0a298ec81 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
@@ -457,6 +457,11 @@ failure. Now such a failure is redelivered as any other, 
from attempt 1, so such
 before. The `CamelRedelivered`, `CamelRedeliveryCounter` and 
`CamelRedeliveryMaxCounter` headers stay on the message
 after the redelivered step succeeded, as before.
 
+The same applies after a `doCatch` caught the failure of a route that the 
`doTry` called (such as with `direct`) and
+whose error handler redelivered it: a later failure (after the `doTry`, in a 
route the `doCatch` or `doFinally` block
+calls, in a calling route when the `doCatch` block throws, or in a route 
called later) is redelivered from attempt 1.
+The redelivery headers of the called route stay readable in the `doCatch` 
block.
+
 === camel-core - a nested split no longer changes the processed items of the 
outer split
 
 With `errorThreshold` or `maxFailedRecords` on a split that contains another 
split, the inner split changed the number of

Reply via email to