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 3a481b4c0de4 CAMEL-25113: camel-core - onException configuration: fix 
bugs found in a deep review (#27019)
3a481b4c0de4 is described below

commit 3a481b4c0de4d7df7e91aee68ccf88065b0d795c
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 29 07:35:35 2026 +0200

    CAMEL-25113: camel-core - onException configuration: fix bugs found in a 
deep review (#27019)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/model/OnExceptionDefinition.java  |   4 +-
 .../org/apache/camel/reifier/CatchReifier.java     |   3 +
 .../reifier/errorhandler/ErrorHandlerReifier.java  |  10 +-
 .../OnExceptionReifierEdgeCasesTest.java           | 149 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   8 ++
 .../modules/ROOT/pages/exception-clause.adoc       |  35 +++--
 .../modules/ROOT/pages/exception-redelivery.adoc   |   6 +
 7 files changed, 199 insertions(+), 16 deletions(-)

diff --git 
a/core/camel-core-model/src/main/java/org/apache/camel/model/OnExceptionDefinition.java
 
b/core/camel-core-model/src/main/java/org/apache/camel/model/OnExceptionDefinition.java
index b43ae8accefa..65b780c381b6 100644
--- 
a/core/camel-core-model/src/main/java/org/apache/camel/model/OnExceptionDefinition.java
+++ 
b/core/camel-core-model/src/main/java/org/apache/camel/model/OnExceptionDefinition.java
@@ -222,8 +222,8 @@ public class OnExceptionDefinition extends 
OutputDefinition<OnExceptionDefinitio
         if (outputs == null || getOutputs().isEmpty()) {
             // no outputs so there should be some sort of configuration
             ObjectHelper.firstNotNull(handledPolicy, handled, continuedPolicy, 
continued, retryWhilePolicy, retryWhile,
-                    redeliveryPolicyType, useOriginalMessage, useOriginalBody, 
onRedeliveryRef,
-                    onRedelivery, onExceptionOccurred)
+                    redeliveryPolicyType, redeliveryPolicyRef, 
useOriginalMessage, useOriginalBody, onRedeliveryRef,
+                    onRedelivery, onExceptionOccurredRef, onExceptionOccurred)
                     .orElseThrow(() -> new IllegalArgumentException(this + " 
is not configured."));
         }
     }
diff --git 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/CatchReifier.java
 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/CatchReifier.java
index ed9d59b33f14..65469b81f900 100644
--- 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/CatchReifier.java
+++ 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/CatchReifier.java
@@ -79,6 +79,9 @@ public class CatchReifier extends 
ProcessorReifier<CatchDefinition> {
         List<Class<? extends Throwable>> answer = new ArrayList<>(list.size());
         for (String name : list) {
             Class<Throwable> type = 
camelContext.getClassResolver().resolveMandatoryClass(name, Throwable.class);
+            if (!Throwable.class.isAssignableFrom(type)) {
+                throw new IllegalArgumentException("The class: " + name + " in 
doCatch is not an exception");
+            }
             answer.add(type);
         }
         return answer;
diff --git 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/errorhandler/ErrorHandlerReifier.java
 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/errorhandler/ErrorHandlerReifier.java
index fe8e307a267c..93bc9bdecdae 100644
--- 
a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/errorhandler/ErrorHandlerReifier.java
+++ 
b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/errorhandler/ErrorHandlerReifier.java
@@ -295,13 +295,15 @@ public abstract class ErrorHandlerReifier<T extends 
ErrorHandlerFactory> extends
         List<Class<? extends Throwable>> list;
         if (ObjectHelper.isNotEmpty(exceptionType.getExceptions())) {
             list = createExceptionClasses(exceptionType);
+            // create the predicate (once) when the route is created (such as 
resolving placeholders of route templates)
+            Predicate when = exceptionType.getOnWhen() != null
+                    ? 
createPredicate(exceptionType.getOnWhen().getExpression()) : null;
             for (Class<? extends Throwable> clazz : list) {
                 String routeId = null;
                 // only get the route id, if the exception type is route scoped
                 if (exceptionType.isRouteScoped()) {
                     routeId = route.getRouteId();
                 }
-                Predicate when = exceptionType.getOnWhen() != null ? 
exceptionType.getOnWhen().getExpression() : null;
                 ExceptionPolicyKey key = new ExceptionPolicyKey(routeId, 
clazz, when);
                 ExceptionPolicy policy = createExceptionPolicy(exceptionType);
                 handlerSupport.addExceptionPolicy(key, policy);
@@ -324,7 +326,11 @@ public abstract class ErrorHandlerReifier<T extends 
ErrorHandlerFactory> extends
     }
 
     protected Class<? extends Throwable> resolveExceptionClass(String name) 
throws ClassNotFoundException {
-        return camelContext.getClassResolver().resolveMandatoryClass(name, 
Throwable.class);
+        Class<? extends Throwable> answer = 
camelContext.getClassResolver().resolveMandatoryClass(name, Throwable.class);
+        if (!Throwable.class.isAssignableFrom(answer)) {
+            throw new IllegalArgumentException("The class: " + name + " in 
onException is not an exception");
+        }
+        return answer;
     }
 
     /**
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionReifierEdgeCasesTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionReifierEdgeCasesTest.java
new file mode 100644
index 000000000000..86578de6f4dd
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionReifierEdgeCasesTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.builder.TemplatedRouteBuilder;
+import org.apache.camel.model.CatchDefinition;
+import org.apache.camel.model.OnExceptionDefinition;
+import org.apache.camel.model.TryDefinition;
+import org.apache.camel.processor.errorhandler.RedeliveryPolicy;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class OnExceptionReifierEdgeCasesTest extends ContextTestSupport {
+
+    @Override
+    public boolean isUseRouteBuilder() {
+        return false;
+    }
+
+    @Test
+    public void testOnWhenWithRouteTemplateParameter() throws Exception {
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                routeTemplate("myTemplate").templateParameter("code")
+                        .from("direct:start")
+                        
.onException(IOException.class).onWhen(simple("${header.code} == 
'{{code}}'")).handled(true)
+                        .to("mock:when").end()
+                        
.onException(IOException.class).handled(true).to("mock:fallback").end()
+                        .throwException(new IOException("Forced"));
+            }
+        });
+        context.start();
+        TemplatedRouteBuilder.builder(context, "myTemplate").parameter("code", 
"abc").add();
+
+        getMockEndpoint("mock:when").expectedMessageCount(1);
+        getMockEndpoint("mock:fallback").expectedMessageCount(0);
+
+        template.sendBodyAndHeader("direct:start", "Hello", "code", "abc");
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    public void testOnWhenInvalidSyntaxFailsOnStartup() {
+        assertThrows(Exception.class, () -> {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    from("direct:start")
+                            
.onException(IOException.class).onWhen(simple("${header.foo 
==")).handled(true).end()
+                            .to("mock:result");
+                }
+            });
+            context.start();
+        });
+    }
+
+    @Test
+    public void testRedeliveryPolicyRefOnly() throws Exception {
+        RedeliveryPolicy policy = new RedeliveryPolicy();
+        policy.setMaximumRedeliveries(2);
+        policy.setRedeliveryDelay(0);
+        context.getRegistry().bind("myPolicy", policy);
+
+        AtomicInteger attempts = new AtomicInteger();
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                onException(IOException.class).redeliveryPolicyRef("myPolicy");
+
+                from("direct:start").process(e -> {
+                    attempts.incrementAndGet();
+                    throw new IOException("Forced");
+                });
+            }
+        });
+        context.start();
+
+        assertThrows(Exception.class, () -> template.sendBody("direct:start", 
"Hello"));
+        assertEquals(3, attempts.get());
+    }
+
+    @Test
+    public void testNotAnExceptionClass() {
+        assertThrows(Exception.class, () -> {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    OnExceptionDefinition def = 
onException(IOException.class).handled(true);
+                    def.getExceptions().clear();
+                    def.getExceptions().add("java.lang.String");
+
+                    from("direct:start").to("mock:result");
+                }
+            });
+            context.start();
+        });
+    }
+
+    @Test
+    public void testDoCatchNotAnExceptionClass() {
+        Exception e = assertThrows(Exception.class, () -> {
+            context.addRoutes(new RouteBuilder() {
+                @Override
+                public void configure() {
+                    TryDefinition doTry = 
from("direct:start").doTry().to("mock:result");
+                    doTry.doCatch(IOException.class);
+                    CatchDefinition doCatch = (CatchDefinition) 
doTry.getOutputs().get(doTry.getOutputs().size() - 1);
+                    // as in XML or YAML, where the classes are resolved from 
their names
+                    doCatch.setExceptionClasses(null);
+                    doCatch.getExceptions().clear();
+                    doCatch.getExceptions().add("java.lang.String");
+                }
+            });
+            context.start();
+        });
+        assertTrue(getRootCause(e).getMessage().contains("is not an 
exception"), e.getMessage());
+    }
+
+    private static Throwable getRootCause(Throwable t) {
+        while (t.getCause() != null) {
+            t = t.getCause();
+        }
+        return t;
+    }
+}
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 b31a927923b0..5cd5607798de 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
@@ -3462,6 +3462,14 @@ overflowing to a negative number.
 The Simple function `${normalizeWhitespace()}` now turns every run of 
whitespace (including tabs and new lines) into
 a single space. Previously only runs of spaces were collapsed and a tab or new 
line was kept as-is.
 
+=== camel-core - onException and doCatch configuration is validated when the 
route is created
+
+The `onWhen` predicate of an `onException` is now created when the route is 
created (as with `doCatch` and
+`onCompletion`), so an invalid `onWhen` expression now fails at startup, 
instead of when an exception occurs.
+
+A class in `onException` or `doCatch` that is not an exception (such as 
`<exception>java.lang.String</exception>` in XML
+or YAML) now fails at startup. Previously it was accepted, and never matched 
any exception.
+
 === camel-debezium - a failed embedded engine is now reported
 
 The Debezium consumers now register a `CompletionCallback` on the embedded 
engine. When the engine stops
diff --git a/docs/user-manual/modules/ROOT/pages/exception-clause.adoc 
b/docs/user-manual/modules/ROOT/pages/exception-clause.adoc
index 31ad69e29351..f02adaca74b9 100644
--- a/docs/user-manual/modules/ROOT/pages/exception-clause.adoc
+++ b/docs/user-manual/modules/ROOT/pages/exception-clause.adoc
@@ -174,16 +174,27 @@ Camel uses *`DefaultExceptionPolicyStrategy`* to 
determine a strategy
 how an exception being thrown should be handled by which *`onException`*
 clause. The strategy is:
 
-* the order in which the *`onException`* is configured takes precedence.
-Camel will test from first...last defined.
-* Camel will start from the bottom (nested caused by) and recursive up
-in the exception hierarchy to find the first matching *`onException`*
-clause.
+* Camel tests the thrown exception, and each of its causes (the nested
+caused by exceptions), against the exceptions of all the *`onException`*
+clauses.
 * *`instanceof`* test is used for testing the given exception with the
-*`onException`* clause defined exception list. An exact *`instanceof`*
-match will always be used, otherwise the *`onException`* clause that has
-an exception that is the closets super of the thrown exception is
-selected (recurring up the exception hierarchy).
+*`onException`* clause defined exception list. An exact match is
+preferred, otherwise the *`onException`* clause that has an exception
+that is the closest super class of the thrown exception is selected
+(recurring up the exception hierarchy).
+* The *closest* match wins, across the thrown exception and all its causes.
+For example, an exact match on the thrown (wrapper) exception wins over a
+super class match on a nested cause. When two clauses are equally close,
+the match on the most nested cause (bottom) wins.
+* When an *`onException`* in a route and an *`onException`* in the
+`RouteBuilder` (context scoped) are equally close, then the one in the
+route wins.
+* An *`onException`* with an `onWhen` predicate only matches when the
+predicate is `true`.
+* If the same exception is configured in more than one *`onException`*
+(in the same scope, without `onWhen`), then the *last* one configured is
+used. The *`onException`* from route configurations are added after the
+ones in the `RouteBuilder`, and are therefore used instead.
 
 This is best illustrated with an exception:
 
@@ -197,9 +208,9 @@ onException(OrderFailedException.class)
     .maximumRedeliveries(2);
 ----
 
-In the sample above we have defined two exceptions in
-which *`IOException`* is first, so Camel will pickup this exception if
-there is a match. *`IOException`* that is more general is selected then.
+In the sample above we have defined two exceptions, and Camel selects the
+*`onException`* that is the closest match of the thrown exception (or one of
+its causes).
 
 So if an exception is thrown with this hierarchy:
 
diff --git a/docs/user-manual/modules/ROOT/pages/exception-redelivery.adoc 
b/docs/user-manual/modules/ROOT/pages/exception-redelivery.adoc
index 703e9eacb1b0..cdd9ffff1a31 100644
--- a/docs/user-manual/modules/ROOT/pages/exception-redelivery.adoc
+++ b/docs/user-manual/modules/ROOT/pages/exception-redelivery.adoc
@@ -18,6 +18,12 @@ information about redeliver and which redeliver options 
exists.
 By default, any xref:exception-clause.adoc[Exception Clause] will *not*
 redeliver! (as it sets the `maximumRedeliveries` option to 0).
 
+NOTE: When any redelivery option is configured on the *`onException`* (such as
+`redeliveryDelay`, `logStackTrace` or `retryAttemptedLogLevel`), then its
+redelivery policy is based on the redelivery policy of the error handler, and
+the `maximumRedeliveries` of the error handler is used (unless
+`maximumRedeliveries` is also configured on the *`onException`*).
+
 Sometimes you want to configure the redelivery policy on a per exception
 type basis. By default in the top examples, if an
 *`org.apache.camel.ValidationException`* occurs then the message will

Reply via email to