This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-24981 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 36451b5a86ae4a683d73bc7e48622b828e8a0d9f Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 23 22:45:04 2026 +0200 CAMEL-24981: error handler uses the onException of the current exception on redelivery When a redelivery attempt failed with a different exception than the previous attempt, the error handler kept the onException matched by the earlier exception: its failure processor, handled/continued predicates, redelivery policy and onRedelivery processor. A new exception with no onException of its own was then handled by the earlier one instead of going to the dead letter channel or back to the caller. handleException now starts from the error handler defaults before applying the policy that matches the current exception. prepare() uses the same defaults, which also clears a failure processor left over on a reused pooled task. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../errorhandler/RedeliveryErrorHandler.java | 25 +++- ...nExceptionChangedExceptionOnRedeliveryTest.java | 141 +++++++++++++++++++++ .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 11 ++ 3 files changed, 172 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..d0dc46cb9156 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionChangedExceptionOnRedeliveryTest.java @@ -0,0 +1,141 @@ +/* + * 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 with IOException, then 2 more as the IllegalArgumentException policy allows 2 redeliveries + 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 eace13660003..c5006cf0ca98 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 @@ -73,6 +73,17 @@ 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. + === Context reload now re-applies placeholder based component options When a context reload is triggered, for example by one of the vault components detecting that a secret was rotated,
