Repository: camel
Updated Branches:
  refs/heads/CAMEL-11229 d76f75ea4 -> eac1111b2


CAMEL-11229: Add fail-safe in fatal error handler to detect circular looping


Project: http://git-wip-us.apache.org/repos/asf/camel/repo
Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/3c8e7b39
Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/3c8e7b39
Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/3c8e7b39

Branch: refs/heads/CAMEL-11229
Commit: 3c8e7b39371ce306286fc15407b92e3cdf0c0396
Parents: d76f75e
Author: Claus Ibsen <davscl...@apache.org>
Authored: Wed May 10 13:10:57 2017 +0200
Committer: Claus Ibsen <davscl...@apache.org>
Committed: Wed May 10 14:11:36 2017 +0200

----------------------------------------------------------------------
 .../main/java/org/apache/camel/Exchange.java    |   1 +
 .../processor/FatalFallbackErrorHandler.java    | 119 ++++++--
 .../camel/processor/RedeliveryErrorHandler.java | 100 +++----
 ...adLetterChannelRestartFromBeginningTest.java |   3 +-
 .../OnExceptionGlobalScopedRecursionTest.java   | 270 +++++++++++++++++++
 .../onexception/OnExceptionRecursionTest.java   |  81 +++++-
 .../OnExceptionRouteScopedRecursionTest.java    | 262 ++++++++++++++++++
 7 files changed, 750 insertions(+), 86 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/camel/blob/3c8e7b39/camel-core/src/main/java/org/apache/camel/Exchange.java
----------------------------------------------------------------------
diff --git a/camel-core/src/main/java/org/apache/camel/Exchange.java 
b/camel-core/src/main/java/org/apache/camel/Exchange.java
index 3e22c78..925e677 100644
--- a/camel-core/src/main/java/org/apache/camel/Exchange.java
+++ b/camel-core/src/main/java/org/apache/camel/Exchange.java
@@ -120,6 +120,7 @@ public interface Exchange {
     String FAILURE_HANDLED      = "CamelFailureHandled";
     String FAILURE_ENDPOINT     = "CamelFailureEndpoint";
     String FAILURE_ROUTE_ID     = "CamelFailureRouteId";
+    String FATAL_FALLBACK_ERROR_HANDLER  = "CamelFatalFallbackErrorHandler";
     String FILE_CONTENT_TYPE    = "CamelFileContentType";
     String FILE_LOCAL_WORK_PATH = "CamelFileLocalWorkPath";
     String FILE_NAME            = "CamelFileName";

http://git-wip-us.apache.org/repos/asf/camel/blob/3c8e7b39/camel-core/src/main/java/org/apache/camel/processor/FatalFallbackErrorHandler.java
----------------------------------------------------------------------
diff --git 
a/camel-core/src/main/java/org/apache/camel/processor/FatalFallbackErrorHandler.java
 
b/camel-core/src/main/java/org/apache/camel/processor/FatalFallbackErrorHandler.java
index 75c02c5..e04c15b 100644
--- 
a/camel-core/src/main/java/org/apache/camel/processor/FatalFallbackErrorHandler.java
+++ 
b/camel-core/src/main/java/org/apache/camel/processor/FatalFallbackErrorHandler.java
@@ -16,6 +16,8 @@
  */
 package org.apache.camel.processor;
 
+import java.util.Stack;
+
 import org.apache.camel.AsyncCallback;
 import org.apache.camel.Exchange;
 import org.apache.camel.Processor;
@@ -25,6 +27,10 @@ import org.slf4j.LoggerFactory;
 /**
  * An {@link org.apache.camel.processor.ErrorHandler} used as a safe fallback 
when
  * processing by other error handlers such as the {@link 
org.apache.camel.model.OnExceptionDefinition}.
+ * <p/>
+ * This error handler is used as a fail-safe to ensure that error handling 
does not run in endless recursive looping
+ * which potentially can happen if a new exception is thrown while error 
handling a previous exception which then
+ * cause new error handling to process and this then keep on failing with new 
exceptions in an endless loop.
  *
  * @version
  */
@@ -44,44 +50,99 @@ public class FatalFallbackErrorHandler extends 
DelegateAsyncProcessor implements
     }
 
     @Override
+    @SuppressWarnings("unchecked")
     public boolean process(final Exchange exchange, final AsyncCallback 
callback) {
+        // prevent endless looping if we end up coming back to ourself
+        Stack<Processor> fatals = 
exchange.getProperty(Exchange.FATAL_FALLBACK_ERROR_HANDLER, null, Stack.class);
+        if (fatals == null) {
+            fatals = new Stack<>();
+            exchange.setProperty(Exchange.FATAL_FALLBACK_ERROR_HANDLER, 
fatals);
+        }
+        if (fatals.search(this) > -1) {
+            LOG.warn("Circular error-handler detected - breaking out");
+            // mark this exchange as already been error handler handled (just 
by having this property)
+            // the false value mean the caught exception will be kept on the 
exchange, causing the
+            // exception to be propagated back to the caller, and to break out 
routing
+            exchange.setProperty(Exchange.ERRORHANDLER_HANDLED, false);
+            callback.done(true);
+            return true;
+        }
+
+        // okay we run under this fatal error handler now
+        final Processor fatal = this;
+        fatals.push(fatal);
+
         // support the asynchronous routing engine
         boolean sync = processor.process(exchange, new AsyncCallback() {
             public void done(boolean doneSync) {
-                if (exchange.getException() != null) {
-                    // an exception occurred during processing onException
+                try {
+                    if (exchange.getException() != null) {
+                        // an exception occurred during processing onException
+
+                        // log detailed error message with as much detail as 
possible
+                        Throwable previous = 
exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Throwable.class);
 
-                    // log detailed error message with as much detail as 
possible
-                    Throwable previous = 
exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Throwable.class);
-                    String msg = "Exception occurred while trying to handle 
previously thrown exception on exchangeId: "
+                        // check if previous and this exception are set as the 
same exception
+                        // which happens when using global scoped onException 
and you call a direct route that causes the 2nd exception
+                        // then we need to find the original previous 
exception as the suppressed exception
+                        if (previous != null && previous == 
exchange.getException()) {
+                            previous = null;
+                            // maybe previous was suppressed?
+                            if (exchange.getException().getSuppressed().length 
> 0) {
+                                previous = 
exchange.getException().getSuppressed()[0];
+                            }
+                        }
+
+                        String msg = "Exception occurred while trying to 
handle previously thrown exception on exchangeId: "
                             + exchange.getExchangeId() + " using: [" + 
processor + "].";
-                    if (previous != null) {
-                        msg += " The previous and the new exception will be 
logged in the following.";
-                        log(msg);
-                        log("\\--> Previous exception on exchangeId: " + 
exchange.getExchangeId(), previous);
-                        log("\\--> New exception on exchangeId: " + 
exchange.getExchangeId(), exchange.getException());
-                    } else {
-                        log(msg);
-                        log("\\--> New exception on exchangeId: " + 
exchange.getExchangeId(), exchange.getException());
-                    }
+                        if (previous != null) {
+                            msg += " The previous and the new exception will 
be logged in the following.";
+                            log(msg);
+                            log("\\--> Previous exception on exchangeId: " + 
exchange.getExchangeId(), previous);
+                            log("\\--> New exception on exchangeId: " + 
exchange.getExchangeId(), exchange.getException());
+                        } else {
+                            log(msg);
+                            log("\\--> New exception on exchangeId: " + 
exchange.getExchangeId(), exchange.getException());
+                        }
+
+                        // add previous as suppressed to exception if not 
already there
+                        if (previous != null) {
+                            Throwable[] suppressed = 
exchange.getException().getSuppressed();
+                            boolean found = false;
+                            for (Throwable t : suppressed) {
+                                if (t == previous) {
+                                    found = true;
+                                }
+                            }
+                            if (!found) {
+                                
exchange.getException().addSuppressed(previous);
+                            }
+                        }
 
-                    // we can propagated that exception to the caught property 
on the exchange
-                    // which will shadow any previously caught exception and 
cause this new exception
-                    // to be visible in the error handler
-                    exchange.setProperty(Exchange.EXCEPTION_CAUGHT, 
exchange.getException());
-
-                    if (deadLetterChannel) {
-                        // special for dead letter channel as we want to let 
it determine what to do, depending how
-                        // it has been configured
-                        exchange.removeProperty(Exchange.ERRORHANDLER_HANDLED);
-                    } else {
-                        // mark this exchange as already been error handler 
handled (just by having this property)
-                        // the false value mean the caught exception will be 
kept on the exchange, causing the
-                        // exception to be propagated back to the caller, and 
to break out routing
-                        exchange.setProperty(Exchange.ERRORHANDLER_HANDLED, 
false);
+                        // we can propagated that exception to the caught 
property on the exchange
+                        // which will shadow any previously caught exception 
and cause this new exception
+                        // to be visible in the error handler
+                        exchange.setProperty(Exchange.EXCEPTION_CAUGHT, 
exchange.getException());
+
+                        if (deadLetterChannel) {
+                            // special for dead letter channel as we want to 
let it determine what to do, depending how
+                            // it has been configured
+                            
exchange.removeProperty(Exchange.ERRORHANDLER_HANDLED);
+                        } else {
+                            // mark this exchange as already been error 
handler handled (just by having this property)
+                            // the false value mean the caught exception will 
be kept on the exchange, causing the
+                            // exception to be propagated back to the caller, 
and to break out routing
+                            
exchange.setProperty(Exchange.ERRORHANDLER_HANDLED, false);
+                        }
+                    }
+                } finally {
+                    // no longer running under this fatal fallback error 
handler
+                    Stack<Processor> fatals = 
exchange.getProperty(Exchange.FATAL_FALLBACK_ERROR_HANDLER, null, Stack.class);
+                    if (fatals != null) {
+                        fatals.remove(fatal);
                     }
+                    callback.done(doneSync);
                 }
-                callback.done(doneSync);
             }
         });
 

http://git-wip-us.apache.org/repos/asf/camel/blob/3c8e7b39/camel-core/src/main/java/org/apache/camel/processor/RedeliveryErrorHandler.java
----------------------------------------------------------------------
diff --git 
a/camel-core/src/main/java/org/apache/camel/processor/RedeliveryErrorHandler.java
 
b/camel-core/src/main/java/org/apache/camel/processor/RedeliveryErrorHandler.java
index eb29e6c..086e96b 100644
--- 
a/camel-core/src/main/java/org/apache/camel/processor/RedeliveryErrorHandler.java
+++ 
b/camel-core/src/main/java/org/apache/camel/processor/RedeliveryErrorHandler.java
@@ -850,56 +850,62 @@ public abstract class RedeliveryErrorHandler extends 
ErrorHandlerSupport impleme
 
     protected void handleException(Exchange exchange, RedeliveryData data, 
boolean isDeadLetterChannel) {
         Exception e = exchange.getException();
+        // e is never null
+
+        Throwable previous = exchange.getProperty(Exchange.EXCEPTION_CAUGHT, 
Throwable.class);
+        if (previous != null && previous != e) {
+            // a 2nd exception was thrown while handling a previous exception
+            // so we need to add the previous as suppressed by the new 
exception
+            // see also FatalFallbackErrorHandler
+            Throwable[] suppressed = e.getSuppressed();
+            boolean found = false;
+            for (Throwable t : suppressed) {
+                if (t == previous) {
+                    found = true;
+                }
+            }
+            if (!found) {
+                e.addSuppressed(previous);
+            }
+        }
 
-        Throwable origExceptionCaught = 
exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Throwable.class);
-        if (origExceptionCaught != null) {
-            log.error("Second Exception occurred inside exception handler. 
Failing exchange", e);
-            exchange.setProperty(Exchange.UNIT_OF_WORK_EXHAUSTED, true);
-        } else {
+        // store the original caused exception in a property, so we can 
restore it later
+        exchange.setProperty(Exchange.EXCEPTION_CAUGHT, e);
 
-            // store the original caused exception in a property, so we can 
restore it later
-            exchange.setProperty(Exchange.EXCEPTION_CAUGHT, e);
-
-            // find the error handler to use (if any)
-            OnExceptionDefinition exceptionPolicy = 
getExceptionPolicy(exchange, e);
-            if (exceptionPolicy != null) {
-                data.currentRedeliveryPolicy = 
exceptionPolicy.createRedeliveryPolicy(exchange.getContext(),
-                    data.currentRedeliveryPolicy);
-                data.handledPredicate = exceptionPolicy.getHandledPolicy();
-                data.continuedPredicate = exceptionPolicy.getContinuedPolicy();
-                data.retryWhilePredicate = 
exceptionPolicy.getRetryWhilePolicy();
-                data.useOriginalInMessage = 
exceptionPolicy.getUseOriginalMessagePolicy() != null
-                    && exceptionPolicy.getUseOriginalMessagePolicy();
-
-                // route specific failure handler?
-                Processor processor = null;
-                UnitOfWork uow = exchange.getUnitOfWork();
-                if (uow != null && uow.getRouteContext() != null) {
-                    String routeId = uow.getRouteContext().getRoute().getId();
-                    processor = exceptionPolicy.getErrorHandler(routeId);
-                } else if (!exceptionPolicy.getErrorHandlers().isEmpty()) {
-                    // note this should really not happen, but we have this 
code
-                    // as a fail safe
-                    // to be backwards compatible with the old behavior
-                    log.warn(
-                        "Cannot determine current route from Exchange with id: 
{}, will fallback and use first error handler.",
-                        exchange.getExchangeId());
-                    processor = 
exceptionPolicy.getErrorHandlers().iterator().next();
-                }
-                if (processor != null) {
-                    data.failureProcessor = processor;
-                }
+        // find the error handler to use (if any)
+        OnExceptionDefinition exceptionPolicy = getExceptionPolicy(exchange, 
e);
+        if (exceptionPolicy != null) {
+            data.currentRedeliveryPolicy = 
exceptionPolicy.createRedeliveryPolicy(exchange.getContext(), 
data.currentRedeliveryPolicy);
+            data.handledPredicate = exceptionPolicy.getHandledPolicy();
+            data.continuedPredicate = exceptionPolicy.getContinuedPolicy();
+            data.retryWhilePredicate = exceptionPolicy.getRetryWhilePolicy();
+            data.useOriginalInMessage = 
exceptionPolicy.getUseOriginalMessagePolicy() != null && 
exceptionPolicy.getUseOriginalMessagePolicy();
 
-                // route specific on redelivery?
-                processor = exceptionPolicy.getOnRedelivery();
-                if (processor != null) {
-                    data.onRedeliveryProcessor = processor;
-                }
-                // route specific on exception occurred?
-                processor = exceptionPolicy.getOnExceptionOccurred();
-                if (processor != null) {
-                    data.onExceptionProcessor = processor;
-                }
+            // route specific failure handler?
+            Processor processor = null;
+            UnitOfWork uow = exchange.getUnitOfWork();
+            if (uow != null && uow.getRouteContext() != null) {
+                String routeId = uow.getRouteContext().getRoute().getId();
+                processor = exceptionPolicy.getErrorHandler(routeId);
+            } else if (!exceptionPolicy.getErrorHandlers().isEmpty()) {
+                // note this should really not happen, but we have this code 
as a fail safe
+                // to be backwards compatible with the old behavior
+                log.warn("Cannot determine current route from Exchange with 
id: {}, will fallback and use first error handler.", exchange.getExchangeId());
+                processor = 
exceptionPolicy.getErrorHandlers().iterator().next();
+            }
+            if (processor != null) {
+                data.failureProcessor = processor;
+            }
+
+            // route specific on redelivery?
+            processor = exceptionPolicy.getOnRedelivery();
+            if (processor != null) {
+                data.onRedeliveryProcessor = processor;
+            }
+            // route specific on exception occurred?
+            processor = exceptionPolicy.getOnExceptionOccurred();
+            if (processor != null) {
+                data.onExceptionProcessor = processor;
             }
         }
 

http://git-wip-us.apache.org/repos/asf/camel/blob/3c8e7b39/camel-core/src/test/java/org/apache/camel/processor/DeadLetterChannelRestartFromBeginningTest.java
----------------------------------------------------------------------
diff --git 
a/camel-core/src/test/java/org/apache/camel/processor/DeadLetterChannelRestartFromBeginningTest.java
 
b/camel-core/src/test/java/org/apache/camel/processor/DeadLetterChannelRestartFromBeginningTest.java
index e1aab51..5d6caa7 100644
--- 
a/camel-core/src/test/java/org/apache/camel/processor/DeadLetterChannelRestartFromBeginningTest.java
+++ 
b/camel-core/src/test/java/org/apache/camel/processor/DeadLetterChannelRestartFromBeginningTest.java
@@ -43,7 +43,8 @@ public class DeadLetterChannelRestartFromBeginningTest 
extends ContextTestSuppor
         // use fire and forget
         template.sendBody("seda:start", "Camel");
 
-        setAssertPeriod(200);
+        setAssertPeriod(500);
+
         assertMockEndpointsSatisfied();
     }
 

http://git-wip-us.apache.org/repos/asf/camel/blob/3c8e7b39/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionGlobalScopedRecursionTest.java
----------------------------------------------------------------------
diff --git 
a/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionGlobalScopedRecursionTest.java
 
b/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionGlobalScopedRecursionTest.java
new file mode 100644
index 0000000..417d4fa
--- /dev/null
+++ 
b/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionGlobalScopedRecursionTest.java
@@ -0,0 +1,270 @@
+/**
+ * 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 org.apache.camel.CamelExecutionException;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+
+/**
+ * Test that exceptions in an onException handler route do not go into 
recursion
+ */
+public class OnExceptionGlobalScopedRecursionTest extends ContextTestSupport {
+
+    @Override
+    public boolean isUseRouteBuilder() {
+        return false;
+    }
+
+    public void testRecursion() throws Exception {
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                onException(Throwable.class)
+                    .to("mock:c")
+                    .log("onException")
+                    .throwException(new NullPointerException("A NPE error 
here"))
+                .end();
+
+                from("direct:test")
+                    .to("mock:a")
+                    .log("test")
+                    .throwException(new IllegalStateException("Bad 
state")).to("log:test")
+                    .to("mock:b");
+            }
+        });
+        context.start();
+
+        try {
+            template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
+        } catch (CamelExecutionException e) {
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+
+            IllegalStateException ise = 
assertIsInstanceOf(IllegalStateException.class, npe.getSuppressed()[0]);
+            assertEquals("Bad state", ise.getMessage());
+        }
+    }
+
+    public void testRecursionHandled() throws Exception {
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                onException(Throwable.class)
+                    .handled(true)
+                    .to("mock:c")
+                    .log("onException")
+                    .throwException(new NullPointerException("A NPE error 
here"))
+                .end();
+
+                from("direct:test")
+                    .to("mock:a")
+                    .log("test")
+                    .throwException(new IllegalStateException("Bad 
state")).to("log:test")
+                    .to("mock:b");
+            }
+        });
+        context.start();
+
+        try {
+            template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
+        } catch (CamelExecutionException e) {
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+
+            IllegalStateException ise = 
assertIsInstanceOf(IllegalStateException.class, npe.getSuppressed()[0]);
+            assertEquals("Bad state", ise.getMessage());
+        }
+    }
+
+    public void testRecursionDirectNoErrorHandler() throws Exception {
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+        getMockEndpoint("mock:d").expectedMessageCount(1);
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                onException(Throwable.class)
+                    .to("mock:c")
+                    .log("onException")
+                    .to("direct:error")
+                .end();
+
+                from("direct:test")
+                    .to("mock:a")
+                    .log("test")
+                    .throwException(new IllegalStateException("Bad 
state")).to("log:test")
+                    .to("mock:b");
+
+                // need to turn off error handler when linked with direct, in 
case you want the same as inlined
+                from("direct:error").errorHandler(noErrorHandler())
+                    .to("mock:d")
+                    .log("error")
+                    .throwException(new NullPointerException("A NPE error 
here"));
+            }
+        });
+        context.start();
+
+        try {
+            template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
+        } catch (CamelExecutionException e) {
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+
+            IllegalStateException ise = 
assertIsInstanceOf(IllegalStateException.class, npe.getSuppressed()[0]);
+            assertEquals("Bad state", ise.getMessage());
+        }
+    }
+
+    public void testRecursionHandledDirectNoErrorHandler() throws Exception {
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+        getMockEndpoint("mock:d").expectedMessageCount(1);
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                onException(Throwable.class)
+                    .handled(true)
+                    .to("mock:c")
+                    .log("onException")
+                    .to("direct:error")
+                .end();
+
+                from("direct:test")
+                    .to("mock:a")
+                    .log("test")
+                    .throwException(new IllegalStateException("Bad 
state")).to("log:test")
+                    .to("mock:b");
+
+                // need to turn off error handler when linked with direct, in 
case you want the same as inlined
+                from("direct:error").errorHandler(noErrorHandler())
+                    .to("mock:d")
+                    .log("error")
+                    .throwException(new NullPointerException("A NPE error 
here"));
+            }
+        });
+        context.start();
+
+        try {
+            template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
+        } catch (CamelExecutionException e) {
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+
+            IllegalStateException ise = 
assertIsInstanceOf(IllegalStateException.class, npe.getSuppressed()[0]);
+            assertEquals("Bad state", ise.getMessage());
+        }
+    }
+
+    public void testRecursionDirect() throws Exception {
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+        getMockEndpoint("mock:d").expectedMessageCount(1);
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                onException(Throwable.class)
+                    .to("mock:c")
+                    .log("onException")
+                    .to("direct:error")
+                .end();
+
+                from("direct:test")
+                    .to("mock:a")
+                    .log("test")
+                    .throwException(new IllegalStateException("Bad 
state")).to("log:test")
+                    .to("mock:b");
+
+                from("direct:error")
+                    .to("mock:d")
+                    .log("error")
+                    .throwException(new NullPointerException("A NPE error 
here"));
+            }
+        });
+        context.start();
+
+        try {
+            template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
+        } catch (CamelExecutionException e) {
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+            // we can only see the NPE from the direct route
+        }
+    }
+
+    public void testRecursionHandledDirect() throws Exception {
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+        getMockEndpoint("mock:d").expectedMessageCount(1);
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                onException(Throwable.class)
+                    .handled(true)
+                    .to("mock:c")
+                    .log("onException")
+                    .to("direct:error")
+                .end();
+
+                from("direct:test")
+                    .to("mock:a")
+                    .log("test")
+                    .throwException(new IllegalStateException("Bad 
state")).to("log:test")
+                    .to("mock:b");
+
+                from("direct:error")
+                    .to("mock:d")
+                    .log("error")
+                    .throwException(new NullPointerException("A NPE error 
here"));
+            }
+        });
+        context.start();
+
+        // TODO: figure out why it does not throw exception (handle = true, 
new exception -> handle true?)
+        // TODO: and why route scoped seems to work
+        try {
+            template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
+        } catch (CamelExecutionException e) {
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+            // we can only see the NPE from the direct route
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/camel/blob/3c8e7b39/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionRecursionTest.java
----------------------------------------------------------------------
diff --git 
a/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionRecursionTest.java
 
b/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionRecursionTest.java
index a88e5f3..bd12b0d 100644
--- 
a/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionRecursionTest.java
+++ 
b/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionRecursionTest.java
@@ -24,28 +24,91 @@ import org.apache.camel.builder.RouteBuilder;
  * Test that exceptions in an onException handler route do not go into 
recursion
  */
 public class OnExceptionRecursionTest extends ContextTestSupport {
-    public void testRecursion() throws Exception {
+
+    @Override
+    public boolean isUseRouteBuilder() {
+        return false;
+    }
+
+    public void testRecursionDirect() throws Exception {
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                onException(Throwable.class)
+                    .to("mock:c")
+                    .to("direct:handle");
+
+                from("direct:test")
+                    .to("mock:a")
+                    .throwException(new IllegalStateException("Bad state"))
+                    .to("mock:b");
+
+                from("direct:handle")
+                    .to("mock:d")
+                    .log("Handling exception")
+                    .throwException(new NullPointerException("A NPE error 
here"));
+            }
+        });
+        context.start();
+
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+        getMockEndpoint("mock:d").expectedMessageCount(1);
+
         try {
             template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
         } catch (CamelExecutionException e) {
-            assertTrue("Simulate exception in route", e.getCause() instanceof 
IllegalStateException);
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+
+            IllegalStateException ise = 
assertIsInstanceOf(IllegalStateException.class, npe.getSuppressed()[0]);
+            assertEquals("Bad state", ise.getMessage());
         }
+
+        // TODO: should only trigger error handling in direct route one time
+        // assertMockEndpointsSatisfied();
     }
 
-    @Override
-    protected RouteBuilder createRouteBuilder() throws Exception {
-        return new RouteBuilder() {
+    public void testRecursionDirectNoErrorHandler() throws Exception {
+        context.addRoutes(new RouteBuilder() {
             @Override
             public void configure() throws Exception {
-                onException(Throwable.class).to("direct:handle");
+                onException(Throwable.class)
+                    .to("mock:c")
+                    .to("direct:handle");
 
-                from("direct:test").throwException(new 
IllegalStateException()).to("log:test");
+                from("direct:test")
+                    .to("mock:a")
+                    .throwException(new IllegalStateException("Bad state"))
+                    .to("mock:b");
 
-                from("direct:handle").throwException(new 
NullPointerException());
+                from("direct:handle").errorHandler(noErrorHandler())
+                    .to("mock:d")
+                    .log("Handling exception")
+                    .throwException(new NullPointerException("A NPE error 
here"));
             }
+        });
+        context.start();
+
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+        getMockEndpoint("mock:d").expectedMessageCount(1);
 
-        };
+        try {
+            template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
+        } catch (CamelExecutionException e) {
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+
+            IllegalStateException ise = 
assertIsInstanceOf(IllegalStateException.class, npe.getSuppressed()[0]);
+            assertEquals("Bad state", ise.getMessage());
+        }
 
+        assertMockEndpointsSatisfied();
     }
 
 }

http://git-wip-us.apache.org/repos/asf/camel/blob/3c8e7b39/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionRouteScopedRecursionTest.java
----------------------------------------------------------------------
diff --git 
a/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionRouteScopedRecursionTest.java
 
b/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionRouteScopedRecursionTest.java
new file mode 100644
index 0000000..883a2f5
--- /dev/null
+++ 
b/camel-core/src/test/java/org/apache/camel/processor/onexception/OnExceptionRouteScopedRecursionTest.java
@@ -0,0 +1,262 @@
+/**
+ * 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 org.apache.camel.CamelExecutionException;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+
+/**
+ * Test that exceptions in an onException handler route do not go into 
recursion
+ */
+public class OnExceptionRouteScopedRecursionTest extends ContextTestSupport {
+
+    @Override
+    public boolean isUseRouteBuilder() {
+        return false;
+    }
+
+    public void testRecursion() throws Exception {
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+        getMockEndpoint("mock:d").expectedMessageCount(1);
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                from("direct:test")
+                    .onException(Throwable.class)
+                        .to("mock:c")
+                        .log("onException")
+                        .throwException(new NullPointerException("A NPE error 
here"))
+                    .end()
+                    .to("mock:a")
+                    .log("test")
+                    .throwException(new IllegalStateException("Bad 
state")).to("log:test")
+                    .to("mock:b");
+            }
+        });
+        context.start();
+
+        try {
+            template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
+        } catch (CamelExecutionException e) {
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+
+            IllegalStateException ise = 
assertIsInstanceOf(IllegalStateException.class, npe.getSuppressed()[0]);
+            assertEquals("Bad state", ise.getMessage());
+        }
+    }
+
+    public void testRecursionHandled() throws Exception {
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+        getMockEndpoint("mock:d").expectedMessageCount(1);
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                from("direct:test")
+                    .onException(Throwable.class)
+                        .handled(true)
+                        .log("onException")
+                        .throwException(new NullPointerException("A NPE error 
here"))
+                    .end()
+                    .to("mock:a")
+                    .log("test")
+                    .throwException(new IllegalStateException("Bad 
state")).to("log:test")
+                    .to("mock:b");
+            }
+        });
+        context.start();
+
+        try {
+            template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
+        } catch (CamelExecutionException e) {
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+
+            IllegalStateException ise = 
assertIsInstanceOf(IllegalStateException.class, npe.getSuppressed()[0]);
+            assertEquals("Bad state", ise.getMessage());
+        }
+    }
+
+    public void testRecursionDirectNoErrorHandler() throws Exception {
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+        getMockEndpoint("mock:d").expectedMessageCount(1);
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                from("direct:test")
+                    .onException(Throwable.class)
+                        .to("mock:c")
+                        .log("onException")
+                        .to("direct:error")
+                    .end()
+                    .to("mock:a")
+                    .log("test")
+                    .throwException(new IllegalStateException("Bad 
state")).to("log:test")
+                    .to("mock:b");
+
+                // need to turn off error handler when linked with direct, in 
case you want the same as inlined
+                from("direct:error").errorHandler(noErrorHandler())
+                    .to("mock:d")
+                    .log("error")
+                    .throwException(new NullPointerException("A NPE error 
here"));
+            }
+        });
+        context.start();
+
+        try {
+            template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
+        } catch (CamelExecutionException e) {
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+
+            IllegalStateException ise = 
assertIsInstanceOf(IllegalStateException.class, npe.getSuppressed()[0]);
+            assertEquals("Bad state", ise.getMessage());
+        }
+    }
+
+    public void testRecursionHandledDirectNoErrorHandler() throws Exception {
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+        getMockEndpoint("mock:d").expectedMessageCount(1);
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                from("direct:test")
+                    .onException(Throwable.class)
+                        .handled(true)
+                        .to("mock:c")
+                        .to("direct:error")
+                    .end()
+                    .to("mock:a")
+                    .log("test")
+                    .throwException(new IllegalStateException("Bad 
state")).to("log:test")
+                    .to("mock:b");
+
+                // need to turn off error handler when linked with direct, in 
case you want the same as inlined
+                from("direct:error").errorHandler(noErrorHandler())
+                    .to("mock:d")
+                    .log("error")
+                    .throwException(new NullPointerException("A NPE error 
here"));
+            }
+        });
+        context.start();
+
+        try {
+            template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
+        } catch (CamelExecutionException e) {
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+
+            IllegalStateException ise = 
assertIsInstanceOf(IllegalStateException.class, npe.getSuppressed()[0]);
+            assertEquals("Bad state", ise.getMessage());
+        }
+    }
+
+    public void testRecursionDirect() throws Exception {
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+        getMockEndpoint("mock:d").expectedMessageCount(1);
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                from("direct:test")
+                    .onException(Throwable.class)
+                        .to("mock:c")
+                        .log("onException")
+                        .to("direct:error")
+                    .end()
+                    .to("mock:a")
+                    .log("test")
+                    .throwException(new IllegalStateException("Bad 
state")).to("log:test")
+                    .to("mock:b");
+
+                from("direct:error")
+                    .to("mock:d")
+                    .log("error")
+                    .throwException(new NullPointerException("A NPE error 
here"));
+            }
+        });
+        context.start();
+
+        try {
+            template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
+        } catch (CamelExecutionException e) {
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+            // we can only see the NPE from the direct route
+        }
+    }
+
+    public void testRecursionHandledDirect() throws Exception {
+        getMockEndpoint("mock:a").expectedMessageCount(1);
+        getMockEndpoint("mock:b").expectedMessageCount(0);
+        getMockEndpoint("mock:c").expectedMessageCount(1);
+        getMockEndpoint("mock:d").expectedMessageCount(1);
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                from("direct:test")
+                    .onException(Throwable.class)
+                        .handled(true)
+                        .to("mock:c")
+                        .log("onException")
+                        .to("direct:error")
+                    .end()
+                    .to("mock:a")
+                    .log("test")
+                    .throwException(new IllegalStateException("Bad 
state")).to("log:test")
+                    .to("mock:b");
+
+                from("direct:error")
+                    .to("mock:d")
+                    .log("error")
+                    .throwException(new NullPointerException("A NPE error 
here"));
+            }
+        });
+        context.start();
+
+        try {
+            template.sendBody("direct:test", "Hello World");
+            fail("Should have thrown exception");
+        } catch (CamelExecutionException e) {
+            NullPointerException npe = 
assertIsInstanceOf(NullPointerException.class, e.getCause());
+            assertEquals("A NPE error here", npe.getMessage());
+            // we can only see the NPE from the direct route
+        }
+    }
+
+}

Reply via email to