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