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 2fe77fee0ce0 CAMEL-24981: error handler uses the onException of the
current exception (#26812)
2fe77fee0ce0 is described below
commit 2fe77fee0ce0c43cd893d3348dd65012b6ea503b
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Sep 24 13:05:43 2026 +0200
CAMEL-24981: error handler uses the onException of the current exception
(#26812)
When a redelivery attempt fails with a different exception than the
previous attempt, the error handler now uses the onException that matches the
new exception. If no onException matches it, the error handler's own settings
apply, for example moving the message to the dead letter channel.
Before this, the error handler kept using the onException matched by the
earlier exception, including its handled, continued, redelivery and
onRedelivery settings, so a new exception with no onException of its own could
be routed and handled by the earlier exception's onException and was seen by
neither the caller nor the dead letter channel.
The redelivery counter is not reset when the exception changes, so the new
policy measures its maximumRedeliveries against the attempts already made. That
is documented in the 4.23 upgrade guide with the case it affects.
Closes #26812
---
.../errorhandler/RedeliveryErrorHandler.java | 25 +++-
...nExceptionChangedExceptionOnRedeliveryTest.java | 143 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 17 +++
3 files changed, 180 insertions(+), 5 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 cb294b6524ef..2e97808c98d8 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
@@ -996,24 +996,35 @@ 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;
+ this.exchange = exchange;
+ this.callback = callback;
+ }
+
+ /**
+ * Uses the behaviour configured on the error handler itself, which an
exception policy (onException) matching
+ * the caught exception can then override.
+ */
+ private void useErrorHandlerDefaults() {
this.retryWhilePredicate = retryWhilePolicy;
this.currentRedeliveryPolicy = redeliveryPolicy;
+ this.failureProcessor = null;
this.handledPredicate = getDefaultHandledPredicate();
+ this.continuedPredicate = null;
this.useOriginalInMessage = useOriginalMessagePolicy;
this.useOriginalInBody = useOriginalBodyPolicy;
this.onRedeliveryProcessor = redeliveryProcessor;
this.onExceptionProcessor =
RedeliveryErrorHandler.this.onExceptionProcessor;
- // 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;
- this.exchange = exchange;
- this.callback = callback;
}
@Override
public void reset() {
this.retryWhilePredicate = null;
this.currentRedeliveryPolicy = null;
+ this.failureProcessor = null;
this.handledPredicate = null;
this.continuedPredicate = null;
this.useOriginalInMessage = false;
@@ -1331,6 +1342,10 @@ public abstract class RedeliveryErrorHandler extends
ErrorHandlerSupport
// store the original caused exception in a property, so we can
restore it later
exchange.setProperty(ExchangePropertyKey.EXCEPTION_CAUGHT, e);
+ // the exception may differ from the one caught on a previous
attempt, so start over from the
+ // error handler defaults and do not keep what a previous
exception policy set (CAMEL-24981)
+ useErrorHandlerDefaults();
+
// find the error handler to use (if any)
ExceptionPolicy exceptionPolicy = getExceptionPolicy(exchange, e);
if (exceptionPolicy != null) {
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionChangedExceptionOnRedeliveryTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionChangedExceptionOnRedeliveryTest.java
new file mode 100644
index 000000000000..527b79aae9e2
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionChangedExceptionOnRedeliveryTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.onexception;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * When the exception changes between redelivery attempts, the exception
policy (onException) for the current exception
+ * is used, and not the one matched by a previous attempt (CAMEL-24981).
+ */
+public class OnExceptionChangedExceptionOnRedeliveryTest extends
ContextTestSupport {
+
+ private final AtomicInteger attempts = new AtomicInteger();
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ attempts.set(0);
+ super.setUp();
+ }
+
+ @Test
+ public void testNoPolicyForNewExceptionGoesToDeadLetter() throws Exception
{
+ getMockEndpoint("mock:io").expectedMessageCount(0);
+ getMockEndpoint("mock:iae").expectedMessageCount(0);
+ getMockEndpoint("mock:dead").expectedMessageCount(1);
+
getMockEndpoint("mock:dead").message(0).exchangeProperty(Exchange.EXCEPTION_CAUGHT)
+ .isInstanceOf(IllegalStateException.class);
+
+ template.sendBody("direct:dlc", "Hello");
+
+ assertMockEndpointsSatisfied();
+ assertEquals(2, attempts.get());
+ }
+
+ @Test
+ public void testNoPolicyForNewExceptionIsNotHandled() throws Exception {
+ getMockEndpoint("mock:io").expectedMessageCount(0);
+
+ CamelExecutionException e = assertThrows(CamelExecutionException.class,
+ () -> template.sendBody("direct:default", "Hello"));
+ assertInstanceOf(IllegalStateException.class, e.getCause());
+
+ assertMockEndpointsSatisfied();
+ assertEquals(2, attempts.get());
+ }
+
+ @Test
+ public void testPolicyForNewExceptionIsUsed() throws Exception {
+ getMockEndpoint("mock:io").expectedMessageCount(0);
+ getMockEndpoint("mock:iae").expectedMessageCount(1);
+ getMockEndpoint("mock:dead").expectedMessageCount(0);
+
+ template.sendBody("direct:iae", "Hello");
+
+ assertMockEndpointsSatisfied();
+ // 1 attempt throwing IOException, 1 redelivery allowed by its policy
which then throws
+ // IllegalArgumentException, and 1 more allowed by that exception's
policy: the redelivery counter is not
+ // reset when the exception changes, so at the third attempt it is
already 2 and 2 <= 2 passes once
+ assertEquals(3, attempts.get());
+ }
+
+ @Test
+ public void testSameExceptionKeepsPolicy() throws Exception {
+ getMockEndpoint("mock:io").expectedMessageCount(1);
+ getMockEndpoint("mock:dead").expectedMessageCount(0);
+
+ template.sendBody("direct:same", "Hello");
+
+ assertMockEndpointsSatisfied();
+ // 1 attempt and 1 redelivery
+ assertEquals(2, attempts.get());
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ errorHandler(deadLetterChannel("mock:dead"));
+
+
onException(IOException.class).maximumRedeliveries(1).redeliveryDelay(0).handled(true).to("mock:io");
+
onException(IllegalArgumentException.class).maximumRedeliveries(2).redeliveryDelay(0).handled(true)
+ .to("mock:iae");
+
+ from("direct:dlc").process(e -> {
+ if (attempts.getAndIncrement() == 0) {
+ throw new IOException("Forced");
+ }
+ throw new IllegalStateException("No policy for this");
+ });
+
+ from("direct:iae").process(e -> {
+ if (attempts.getAndIncrement() == 0) {
+ throw new IOException("Forced");
+ }
+ throw new IllegalArgumentException("Has its own policy");
+ });
+
+ from("direct:same").process(e -> {
+ attempts.incrementAndGet();
+ throw new IOException("Forced");
+ });
+
+ from("direct:default").errorHandler(defaultErrorHandler())
+
.onException(IOException.class).maximumRedeliveries(1).redeliveryDelay(0).handled(true)
+ .to("mock:io").end()
+ .process(e -> {
+ if (attempts.getAndIncrement() == 0) {
+ throw new IOException("Forced");
+ }
+ throw new IllegalStateException("No policy for
this");
+ });
+ }
+ };
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 1778be005f4a..0785109b9221 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -106,6 +106,23 @@ Prior to Camel 4.23 the property was only set when there
was no fallback and was
so a fallback that tested it for `null` must now test for `true` or `false`
instead.
`CamelCircuitBreakerResponseShortCircuited` is unchanged and remains `true`
whenever the fallback runs, whatever the cause.
+=== Error handler - onException when the exception changes during redelivery
+
+When a redelivery attempt fails with a different exception than the previous
attempt, the error handler now uses
+the `onException` that matches the new exception. If no `onException` matches
it, the error handler's own settings
+apply, for example moving the message to the dead letter channel.
+
+Prior to Camel 4.23 the error handler kept using the `onException` matched by
the earlier exception, including its
+`handled`, `continued`, redelivery and `onRedelivery` settings. So a new
exception with no `onException` of its own
+could be routed and handled by the earlier exception's `onException`, and was
not seen by the caller or the dead
+letter channel.
+
+The redelivery counter is not reset when the exception changes, so the new
`onException` measures its
+`maximumRedeliveries` against the attempts already made. A route whose
`onException(IOException.class)` allows 5
+redeliveries and which fails 4 times before the exception changes leaves the
new policy a counter of 4, so an
+`onException(IllegalArgumentException.class).maximumRedeliveries(2)` is
already exhausted and the message goes to
+the dead letter channel on the next failure.
+
=== Weighted Load Balancer EIP
The distribution ratios of the weighted load balancer are now validated when
the route starts. A negative ratio,