shashank created CAMEL-26328:
--------------------------------

             Summary: camel-core - Failover load balancer: fails over on an 
exception that is only suppressed (such as one thrown in a doFinally) and drops 
the exception that failed the attempt
                 Key: CAMEL-26328
                 URL: https://issues.apache.org/jira/browse/CAMEL-26328
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: shashank
            Assignee: shashank


The failover load balancer with a list of exceptions fails over when the 
exception that failed the attempt is not in the list but one of its 
*suppressed* exceptions is. Since CAMEL-25114 (4.23.0) an exception thrown in a 
{{doFinally}} block is added as a suppressed exception to the pending exception 
of the {{doTry}} block, so a cleanup step that throws one of the listed types 
now makes the load balancer fail over. When the next processor succeeds, the 
exception that failed the attempt is lost and the route goes on (main 
ffab48ea514b):
{code:java}
from("direct:start")
    .loadBalance().failover(IllegalArgumentException.class)
        .to("direct:attempt")
        .to("mock:second")
    .end()
    .to("mock:after");

from("direct:attempt")
    .doTry()
        .throwException(new IOException("original"))
    .doFinally()
        .throwException(new IllegalArgumentException("cleanup"))
    .end();

main / 4.23.0: exception=null, mock:second=1, mock:after=1   (failed over on 
the doFinally exception, IOException lost)
4.22.1:        exception=IOException("original"), no failover
{code}
The same happens when the {{doFinally}} calls a route that throws. 
{{onException(IllegalArgumentException.class)}} and 
{{doCatch(IllegalArgumentException.class)}} do not match this IOException: they 
look at the exception and its causes only (doTry-eip.adoc, "How doCatch matches 
an exception"; {{DefaultExceptionPolicyStrategy}} and {{CatchProcessor}} walk 
{{ObjectHelper.createExceptionIterable}}). The failover page says the load 
balancer "can be configured to only apply for a specific set of exceptions", 
and does not mention suppressed exceptions.

*Cause*: {{FailOverLoadBalancer.shouldFailOver}} checks each listed type with 
{{exchange.getException(type)}}. That goes to 
{{ObjectHelper.getException(Class, Throwable)}}, which looks at the suppressed 
exceptions first, then at the cause chain. That lookup was added for CAMEL-6406 
(2.16.0) as a general API change, not for failover. Before CAMEL-25114 the 
suppressed exceptions that camel-core added on the routing path were mostly the 
other way round: when an {{onException}} block fails while it handles an 
exception, {{FatalFallbackErrorHandler}} adds the original exception as a 
suppressed exception to the new one (also on camel-4.22.x), so there the 
suppressed exception is the one the attempt first failed with. A failing 
{{onCompletion}} in {{modeBeforeConsumer}} also adds its exception as 
suppressed (camel-4.22.x too). The CAMEL-25114 PR did not look at the load 
balancer.

*Fix* (proposed): {{shouldFailOver}} matches the listed types against the 
exception and its causes only, with {{ObjectHelper.createExceptionIterable}}, 
as {{onException}} and {{doCatch}} do. A paragraph on the failover EIP page 
(and its catalog copy), and a 4.24 upgrade guide entry, because a listed 
exception that is only suppressed no longer causes failover in these cases 
either: an {{onException}} block that fails while it handles a listed exception 
(the exchange fails with the new exception), a {{doCatch}} whose {{onWhen}} 
throws (since CAMEL-25114 the exchange fails with the predicate's exception, 
the original one suppressed), and a processor or component that adds one 
itself. A route that relied on it adds the type of the exception the attempt 
fails with to the list. Not changed: {{ThrottlingExceptionRoutePolicy}} checks 
its {{throttledExceptions}} with the same {{exchange.getException(type)}} loop 
(a follow-up).

*Tests*: {{FailOverLoadBalanceSuppressedExceptionTest}} (camel-core, 11 tests). 
A doFinally that throws a listed exception, a doFinally that calls a route that 
throws one, the same with inheritErrorHandler, round robin and 
maximumFailoverAttempts, a processor that adds one as suppressed, an 
onException block that fails while handling a listed exception, and a doCatch 
whose onWhen throws: no failover, and the exchange fails with the exception of 
the attempt with the listed one as its suppressed exception. These 6 fail on 
main and pass with the fix. Controls that pass on both: a listed exception as 
the cause of an unlisted one fails over, a listed exception thrown directly 
fails over, a listed exception from doTry with an unlisted one from doFinally 
fails over, an unlisted exception does not, and {{onException}} does not match 
the suppressed exception. The load balancer, failover and 
doTry/doCatch/doFinally tests of camel-core pass (196 tests).

*How it was found*: a Rocq specification of Camel's EIPs written from the 
documentation, compared with real Camel on generated routes (a failover 
monitoring IllegalArgumentException whose attempt's doFinally called a route 
that threw one), then reproduced on HotSpot against 4.22.1 and main.

*Affected*: the doFinally case: 4.23.0 and main (camel-4.22.x does not add the 
doFinally exception as suppressed; 4.22.1 checked). A processor or component 
that adds a suppressed exception itself: every version since 2.16.0. A failing 
{{onCompletion}} in {{modeBeforeConsumer}}: 4.0.0 and later (checked on the 
4.0.0 tag).

_Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to