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 13e006eee30c CAMEL-26447: camel-core - after a doCatch caught the 
failure of a called route, the exchange is no longer failure handled (#29584)
13e006eee30c is described below

commit 13e006eee30c4aeab9fe1ca2c4ba1fbaa3096e9c
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Nov 12 00:21:17 2026 +0530

    CAMEL-26447: camel-core - after a doCatch caught the failure of a called 
route, the exchange is no longer failure handled (#29584)
    
    In doTry, after a doCatch caught the failure of a called route that failed 
unhandled, the exchange is no
    longer marked as failure handled, so the later error handlers of the 
exchange no longer treat it as done.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/camel/processor/CatchProcessor.java |  33 +--
 ...DoCatchCalledRouteAfterUnhandledCalleeTest.java |  15 +-
 .../processor/DoCatchFailureHandledMarkTest.java   | 229 +++++++++++++++++++++
 .../ManagedDoCatchFailuresHandledTest.java         |  77 +++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  25 ++-
 5 files changed, 348 insertions(+), 31 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 c962e2e7a7dd..6bce93ffa83c 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
@@ -179,19 +179,16 @@ public class CatchProcessor extends 
BaseDelegateProcessorSupport
         exchange.getExchangeExtension().setRedeliveryExhausted(false);
 
         // a route called from the doTry (such as with direct) that failed 
unhandled has marked the exchange as failure
-        // handled, and as not handled by the error handler, before the 
exception came back here. A failure in a route
-        // that the doCatch block calls must be handled by that route's error 
handler as usual (which does not handle a
-        // new failure of an exchange that is failure handled, and keeps a 
"not handled" that is already set), as when
-        // the doTry failed in this route. So both marks are cleared when the 
doCatch takes over the exception
+        // handled, and as not handled by the error handler, before the 
exception came back here. The doCatch handles
+        // that failure, so the exchange must carry neither mark, as when the 
doTry failed in this route: an error
+        // handler does not handle a new failure of an exchange that is 
failure handled (in a route the doCatch block
+        // calls, later in this route, in a route the doFinally calls, or in a 
calling route), and the readers of the
+        // mark at the end of the exchange (such as the idempotent consumer) 
would treat the message as not processed.
+        // So both marks are cleared when the doCatch takes over the 
exception, and neither is set again after the block
+        // (CAMEL-26447): a failure that the block handles itself (such as a 
dead letter channel of a route it calls) is
+        // marked by that error handler
         final ExchangeExtension ext = exchange.getExchangeExtension();
-        final boolean failureHandled = ext.isFailureHandled();
-        // a primitive "set and true" snapshot, as the doFinally does: a "not 
handled" of the caught failure is not set
-        // again, as the doCatch handled that failure (as when the doTry 
failed in this route). No known case reaches
-        // the restore of a true mark today (a failure that an error handler 
handled has no exception left for a doCatch
-        // to catch); it is kept for symmetry with FinallyProcessor
-        final boolean errorHandlerHandled = ext.isErrorHandlerHandledSet() && 
ext.isErrorHandlerHandled();
-        final boolean restore = failureHandled || 
ext.isErrorHandlerHandledSet();
-        if (restore) {
+        if (ext.isFailureHandled() || ext.isErrorHandlerHandledSet()) {
             ext.setFailureHandled(false);
             ext.setErrorHandlerHandled(null);
         }
@@ -212,18 +209,6 @@ public class CatchProcessor extends 
BaseDelegateProcessorSupport
                 // always clear redelivery exhausted in a catch clause
                 exchange.getExchangeExtension().setRedeliveryExhausted(false);
 
-                if (restore) {
-                    // set the marks of the earlier failure again, unless a 
route called from the block set the handled
-                    // mark itself (such as its dead letter channel that 
handled its failure); failure handled is set
-                    // again but never cleared, so a failure that the block 
handled itself stays marked
-                    if (errorHandlerHandled && 
!ext.isErrorHandlerHandledSet()) {
-                        ext.setErrorHandlerHandled(true);
-                    }
-                    if (failureHandled) {
-                        ext.setFailureHandled(true);
-                    }
-                }
-
                 if (rollbackOnly || rollbackOnlyLast || stop) {
                     exchange.setRouteStop(stop);
                     exchange.setRollbackOnly(rollbackOnly);
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/DoCatchCalledRouteAfterUnhandledCalleeTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/DoCatchCalledRouteAfterUnhandledCalleeTest.java
index 1312c6e9d6b1..d6ee05faadbf 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/processor/DoCatchCalledRouteAfterUnhandledCalleeTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/DoCatchCalledRouteAfterUnhandledCalleeTest.java
@@ -25,11 +25,11 @@ 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.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * The doTry calls a route that fails unhandled (default error handler), and 
the doCatch then calls a route with an
@@ -368,9 +368,9 @@ class DoCatchCalledRouteAfterUnhandledCalleeTest extends 
ContextTestSupport {
         assertNull(out.getException());
         assertEquals(1, count("mock:catch"), "mock:catch");
         assertEquals(1, count("mock:after"), "mock:after");
-        // failure handled is set again after the doCatch block, as before; 
the "not handled" of the caught failure is
-        // not, as when the doTry failed in this route
-        assertTrue(out.getExchangeExtension().isFailureHandled(), "failure 
handled");
+        // neither the failure handled mark nor the "not handled" of the 
caught failure is set again after the doCatch
+        // block, as when the doTry failed in this route (CAMEL-26447)
+        assertFalse(out.getExchangeExtension().isFailureHandled(), "failure 
handled");
         assertNull(out.getExchangeExtension().getErrorHandlerHandled(), 
"handled by the error handler");
     }
 
@@ -381,7 +381,9 @@ class DoCatchCalledRouteAfterUnhandledCalleeTest extends 
ContextTestSupport {
         assertEquals("in catch", out.getException().getMessage());
         assertEquals(1, count("mock:catch"), "mock:catch");
         assertEquals(0, count("mock:after"), "mock:after");
-        assertTrue(out.getExchangeExtension().isFailureHandled(), "failure 
handled");
+        // the failure handled mark of the caught failure is not set again 
(CAMEL-26447), as when the doTry failed in
+        // this route 
(DoCatchFailureHandledMarkTest.failureOfTheDoCatchIsNotFailureHandled)
+        assertFalse(out.getExchangeExtension().isFailureHandled(), "failure 
handled");
         assertNull(out.getExchangeExtension().getErrorHandlerHandled(), 
"handled by the error handler");
     }
 
@@ -435,7 +437,8 @@ class DoCatchCalledRouteAfterUnhandledCalleeTest extends 
ContextTestSupport {
 
     @Test
     void secondDoTryAfterCaughtCalleeFailure() {
-        // the failure handled mark that is set again after the first doCatch 
is cleared again by the second doCatch
+        // the first doCatch clears the failure handled mark of the caught 
failure, so the second doTry runs as in
+        // the route
         assertSecondDoTry(send("direct:secondDoTryViaCallee"));
     }
 
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/DoCatchFailureHandledMarkTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/DoCatchFailureHandledMarkTest.java
new file mode 100644
index 000000000000..9686668f982b
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/DoCatchFailureHandledMarkTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.io.IOException;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.ProcessorDefinition;
+import org.apache.camel.model.TryDefinition;
+import org.apache.camel.spi.IdempotentRepository;
+import 
org.apache.camel.support.processor.idempotent.MemoryIdempotentRepository;
+import org.junit.jupiter.api.Test;
+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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * After a doCatch caught the failure of a route that the doTry called and 
that failed unhandled, the exchange carries
+ * no failure handled mark, as when the doTry failed in the route itself: the 
error handlers that come later handle new
+ * failures, and the readers of the mark at the end of the exchange see a 
message that was processed. Each case runs
+ * twice: with the doTry calling the failing route ("viaCallee") and with the 
doTry failing in the route ("inRoute"),
+ * with the same expected outcome.
+ */
+class DoCatchFailureHandledMarkTest extends ContextTestSupport {
+
+    private final IdempotentRepository viaCalleeRepository = 
MemoryIdempotentRepository.memoryIdempotentRepository();
+    private final IdempotentRepository inRouteRepository = 
MemoryIdempotentRepository.memoryIdempotentRepository();
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                for (String variant : new String[] { "viaCallee", "inRoute" }) 
{
+                    // a later failure in the calling route, after the doTry
+                    failingTry(from("direct:laterFailure-" + variant)
+                            
.onException(IOException.class).handled(true).to("mock:callerHandled").end(), 
variant)
+                            .doCatch(IllegalArgumentException.class)
+                                .to("mock:catch")
+                            .end()
+                            .throwException(new IOException("after the 
doTry"));
+
+                    // a route called from the doFinally after such a doCatch
+                    failingTry(from("direct:finallyAfterCatch-" + variant), 
variant)
+                            .doCatch(IllegalArgumentException.class)
+                                .to("mock:catch")
+                            .doFinally()
+                                .to("direct:dlcRoute")
+                            .end();
+
+                    // the doCatch throws, inside a route called by a route 
with a dead letter channel
+                    from("direct:outer-" + variant)
+                            
.errorHandler(deadLetterChannel("mock:outerDead").redeliveryDelay(0))
+                            .to("direct:inner-" + variant);
+                    failingTry(from("direct:inner-" + variant), variant)
+                            .doCatch(IllegalArgumentException.class)
+                                .throwException(new IllegalStateException("in 
catch"))
+                            .end();
+
+                    // a route called with a plain to after the doTry
+                    failingTry(from("direct:plainTo-" + variant), variant)
+                            .doCatch(IllegalArgumentException.class)
+                                .to("mock:catch")
+                            .end()
+                            .to("direct:dlcRoute")
+                            .to("mock:after");
+
+                    // an idempotent consumer: the message was processed (the 
doCatch handled its failure), so its id
+                    // is confirmed and a duplicate is skipped
+                    failingTry(from("direct:idempotent-" + variant)
+                            .idempotentConsumer(header("id"), 
repository(variant)), variant)
+                            .doCatch(IllegalArgumentException.class)
+                                .to("mock:catch")
+                            .end()
+                            .end();
+
+                    // the advice of the upgrade guide: the doCatch block 
throws, the exchange fails, and the
+                    // idempotent consumer removes the id (removeOnFailure, 
the default), so the message is processed
+                    // when it comes again
+                    failingTry(from("direct:rethrow-" + variant)
+                            .idempotentConsumer(header("id"), 
repository(variant)), variant)
+                            .doCatch(IllegalArgumentException.class)
+                                .to("mock:catch")
+                                .throwException(new 
IllegalStateException("rethrown"))
+                            .end()
+                            .end();
+                }
+
+                // the callee fails unhandled (default error handler)
+                from("direct:failing").throwException(new 
IllegalArgumentException("callee"));
+
+                // the doTry fails with an exchange marked as handled by an 
error handler (both marks, as an error
+                // handler that handled a failure sets them). Such an exchange 
rarely gets to a doCatch (for example an
+                // aggregation strategy that combines a part whose failure was 
handled with a part that failed), so the
+                // marks are set directly
+                from("direct:markedFailure")
+                        .doTry()
+                        .process(e -> {
+                            e.getExchangeExtension().setFailureHandled(true);
+                            
e.getExchangeExtension().setErrorHandlerHandled(true);
+                            throw new IllegalArgumentException("marked");
+                        })
+                        .doCatch(IllegalArgumentException.class)
+                        .to("mock:catch")
+                        .end();
+
+                
from("direct:dlcRoute").errorHandler(deadLetterChannel("mock:dead").redeliveryDelay(0))
+                        .throwException(new IOException("in called route"));
+            }
+        };
+    }
+
+    // a doTry that calls a route that fails unhandled (viaCallee), or that 
fails in the route itself (inRoute)
+    private static TryDefinition failingTry(ProcessorDefinition<?> parent, 
String variant) {
+        TryDefinition doTry = parent.doTry();
+        if ("viaCallee".equals(variant)) {
+            return doTry.to("direct:failing");
+        }
+        return doTry.throwException(new IllegalArgumentException("in route"));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "viaCallee", "inRoute" })
+    void laterFailureIsHandledByTheOnException(String variant) {
+        Exchange out = template.send("direct:laterFailure-" + variant, e -> 
e.getMessage().setBody("hello"));
+        assertNull(out.getException(), "the onException of the calling route 
must handle the later failure");
+        assertEquals(1, count("mock:catch"), "mock:catch");
+        assertEquals(1, count("mock:callerHandled"), "mock:callerHandled");
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "viaCallee", "inRoute" })
+    void routeCalledFromTheDoFinallyHandlesItsFailure(String variant) {
+        Exchange out = template.send("direct:finallyAfterCatch-" + variant, e 
-> e.getMessage().setBody("hello"));
+        assertEquals(1, count("mock:catch"), "mock:catch");
+        assertEquals(1, count("mock:dead"), "the dead letter channel of the 
route called from the doFinally");
+        assertNull(out.getException());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "viaCallee", "inRoute" })
+    void failureOfTheDoCatchGoesToTheDeadLetterChannelOfTheOuterRoute(String 
variant) {
+        Exchange out = template.send("direct:outer-" + variant, e -> 
e.getMessage().setBody("hello"));
+        assertEquals(1, count("mock:outerDead"), "the dead letter channel of 
the outer route");
+        assertNull(out.getException());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "viaCallee", "inRoute" })
+    void failureOfTheDoCatchIsNotFailureHandled(String variant) {
+        // the route whose doCatch throws, called directly: its default error 
handler does not handle the new failure
+        Exchange out = template.send("direct:inner-" + variant, e -> 
e.getMessage().setBody("hello"));
+        assertInstanceOf(IllegalStateException.class, out.getException());
+        assertFalse(out.getExchangeExtension().isFailureHandled(), "failure 
handled");
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "viaCallee", "inRoute" })
+    void routeCalledLaterHandlesItsFailure(String variant) {
+        Exchange out = template.send("direct:plainTo-" + variant, e -> 
e.getMessage().setBody("hello"));
+        assertEquals(1, count("mock:catch"), "mock:catch");
+        assertEquals(1, count("mock:dead"), "the dead letter channel of the 
route called after the doTry");
+        // the called route handled its failure, so the calling route does not 
continue
+        assertEquals(0, count("mock:after"), "mock:after");
+        assertNull(out.getException());
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "viaCallee", "inRoute" })
+    void processedMessageIsConfirmedByTheIdempotentConsumer(String variant) {
+        Exchange out = template.send("direct:idempotent-" + variant, e -> 
e.getMessage().setHeader("id", "m1"));
+        assertNull(out.getException());
+        assertFalse(out.getExchangeExtension().isFailureHandled(), "failure 
handled");
+        assertTrue(repository(variant).contains("m1"), "the id of a processed 
message is kept");
+
+        template.send("direct:idempotent-" + variant, e -> 
e.getMessage().setHeader("id", "m1"));
+        assertEquals(1, count("mock:catch"), "a duplicate is skipped");
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "viaCallee", "inRoute" })
+    void rethrowInTheDoCatchRemovesTheIdempotentId(String variant) {
+        Exchange out = template.send("direct:rethrow-" + variant, e -> 
e.getMessage().setHeader("id", "m2"));
+        assertInstanceOf(IllegalStateException.class, out.getException());
+        assertFalse(repository(variant).contains("m2"), "the id of a failed 
message is removed");
+
+        template.send("direct:rethrow-" + variant, e -> 
e.getMessage().setHeader("id", "m2"));
+        assertEquals(2, count("mock:catch"), "the message is processed again");
+    }
+
+    @Test
+    void doCatchLeavesNoHandledMarkOfTheCaughtFailure() {
+        // the doCatch handled the failure: the exchange carries neither mark 
of it, and never one without the other
+        Exchange out = template.send("direct:markedFailure", e -> 
e.getMessage().setBody("hello"));
+        assertNull(out.getException());
+        assertEquals(1, count("mock:catch"), "mock:catch");
+        assertFalse(out.getExchangeExtension().isFailureHandled(), "failure 
handled");
+        assertFalse(out.getExchangeExtension().isErrorHandlerHandledSet(), 
"handled by the error handler");
+    }
+
+    private IdempotentRepository repository(String variant) {
+        return "viaCallee".equals(variant) ? viaCalleeRepository : 
inRouteRepository;
+    }
+
+    private int count(String uri) {
+        return getMockEndpoint(uri).getReceivedCounter();
+    }
+}
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedDoCatchFailuresHandledTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedDoCatchFailuresHandledTest.java
new file mode 100644
index 000000000000..50dda2a345c6
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedDoCatchFailuresHandledTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.management;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static 
org.apache.camel.management.DefaultManagementObjectNameStrategy.TYPE_ROUTE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A doCatch that caught the failure of a route that the doTry called (and 
that failed unhandled) handled the failure,
+ * so the calling route does not count a handled failure, as when the doTry 
failed in the route itself.
+ */
+@DisabledOnOs(OS.AIX)
+class ManagedDoCatchFailuresHandledTest extends ManagementTestSupport {
+
+    @ParameterizedTest
+    @ValueSource(strings = { "viaCallee", "inRoute" })
+    void doCatchIsNotCountedAsAFailureHandled(String variant) throws Exception 
{
+        getMockEndpoint("mock:catch").expectedMessageCount(1);
+
+        template.sendBody("direct:" + variant, "Hello World");
+
+        assertMockEndpointsSatisfied();
+
+        MBeanServer mbeanServer = getMBeanServer();
+        ObjectName on = getCamelObjectName(TYPE_ROUTE, variant);
+        assertEquals(1L, mbeanServer.getAttribute(on, "ExchangesCompleted"));
+        assertEquals(0L, mbeanServer.getAttribute(on, "ExchangesFailed"));
+        assertEquals(0L, mbeanServer.getAttribute(on, "FailuresHandled"));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:viaCallee").routeId("viaCallee")
+                        .doTry()
+                            .to("direct:failing")
+                        .doCatch(IllegalArgumentException.class)
+                            .to("mock:catch")
+                        .end();
+                from("direct:inRoute").routeId("inRoute")
+                        .doTry()
+                            .throwException(new IllegalArgumentException("in 
route"))
+                        .doCatch(IllegalArgumentException.class)
+                            .to("mock:catch")
+                        .end();
+                // fails unhandled (default error handler)
+                from("direct:failing").routeId("failing")
+                        .throwException(new 
IllegalArgumentException("callee"));
+            }
+        };
+    }
+}
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 11da3239c17c..fbdd21a41b32 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
@@ -556,7 +556,7 @@ strings to `int[]` or `byte[]`, now gives no conversion 
(`null`, or `NoTypeConve
 mandatory conversion), and other type converters can try. Before, it failed 
with a `TypeConversionException` due to
 `IllegalArgumentException: argument type mismatch` or an `ArrayStoreException`.
 
-=== camel-core - a route called from a doCatch handles its failure after the 
doTry called a route that failed
+=== camel-core - after the doTry called a route that failed, a doCatch clears 
the failure marks of the exchange
 
 When the `doTry` block calls a route (such as with `direct`) that fails 
without handling the failure (no `onException`
 handles it, and no dead letter channel), the error handler of a route called 
from the `doCatch` block now runs as it
@@ -584,6 +584,29 @@ The exchange no longer keeps the marks of the caught 
failure, which also changes
 To keep the exchange failing after one attempt as before, give the route 
called from the `doCatch` no error handler
 (`noErrorHandler()`); this applies to every caller of that route.
 
+After the `doCatch` block, the exchange is no longer marked as failure handled 
either (CAMEL-26447), as when the
+`doTry` failed in the same route. So the error handlers that come later handle 
a new failure as usual (they redeliver,
+run their `onException` and dead letter channel, and log an exhausted 
failure): an `onException` of the calling route
+for a failure after the `doTry`, the error handler of a route called later 
(with a plain `to`, or from the
+`doFinally`), and the dead letter channel of a calling route when the 
`doCatch` block throws. Before, they treated the
+exchange as done, and the new failure went back to the caller unhandled. When 
such an error handler now handles the
+new failure, the exchange completes, so its consumer commits or acknowledges 
the message, as for any handled failure.
+
+The readers of the mark at the end of the exchange now see a message that was 
processed, as for a `doTry` that failed
+in the same route:
+
+* the Idempotent Consumer confirms the message id instead of removing it, so 
the message is not processed again;
+* a resumable consumer records the offset of the message;
+* the `failuresHandled` counters (JMX, and the Micrometer and OpenTelemetry 
route policies) no longer count it;
+* with `exactlyOnce` on a Kafka consumer, the output transaction is committed 
together with the offsets (as the Kafka
+  component documents for sends in a `doCatch` block) instead of being 
aborted, unless the caught failure was a Kafka
+  send failure.
+
+There is no option for the old end-of-exchange outcome. For the Idempotent 
Consumer, throw an exception from the
+`doCatch` block (for example with `throwException`): the exchange then fails 
(or an error handler of the route handles
+the new failure), and with `removeOnFailure` (the default) the message id is 
removed, so the message is processed again
+when it comes again.
+
 === camel-core - Simple: more predicates inside ${ } are read as predicates
 
 A predicate written inside the braces (CAMEL-24921, 4.23) is now also read as 
one when it uses the operators

Reply via email to