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