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)