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,

Reply via email to