shashank created CAMEL-26333:
--------------------------------

             Summary: camel-core - doTry: when the doTry calls a route whose 
error handler handled a failure (dead letter channel or handled onException), 
the doFinally block runs only its first step
                 Key: CAMEL-26333
                 URL: https://issues.apache.org/jira/browse/CAMEL-26333
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: shashank
            Assignee: shashank


When a {{doTry}} calls a route (for example with {{direct:}}) whose error 
handler handles a failure (a dead letter channel, or 
{{onException(...).handled(true)}}), the {{doFinally}} block runs only its 
first step and skips the rest, silently (main 8e0ce2d21371, and the released 
4.22.1 and 4.23.0 jars):
{code:java}
errorHandler(deadLetterChannel("mock:dead"));          // or: 
onException(IllegalArgumentException.class).handled(true)

from("direct:a")
    .doTry()
        .to("direct:b")
    .doFinally()
        .to("mock:f1").to("mock:f2").setHeader("f3", 
constant("yes")).to("mock:f4")
    .end()
    .to("mock:end");
from("direct:b").setBody(constant("in-b")).throwException(new 
IllegalArgumentException("x"));

direct:b fails:           mock:dead 1, mock:f1 1, mock:f2 0, header f3 null, 
mock:f4 0, mock:end 0
direct:b does not fail:   mock:dead 0, mock:f1 1, mock:f2 1, header f3 yes,  
mock:f4 1, mock:end 1
{code}
The doFinally EIP is described (model and catalog) as steps "to always execute 
after a doTry block completes, regardless of whether an exception was thrown or 
caught". The steps that are skipped are the ones users put there because they 
must always run (release a lock, write an audit record, compensate).

Cause: the called route's error handler marks the exchange as handled by the 
error handler ({{errorHandlerHandled}}) and as failure handled 
({{failureHandled}}). {{FinallyProcessor}} clears the exception before it runs 
the block, but not those marks. The doFinally block is a {{Pipeline}}, which 
always runs its first step and then stops before each later step when the 
exchange is handled by the error handler 
({{PipelineHelper.continueProcessing}}, "Handled by the error handler"). And an 
error handler does not handle a new failure of an exchange that is failure 
handled, so a route called from such a doFinally cannot handle or continue its 
own failure with its error handler.

A second consequence: a routing slip or dynamic router can route to the 
endpoints of a recipient list instead of its own. A recipient list is a 
pipeline of two processors: the first evaluates the recipients into the 
exchange property {{CamelEvaluateExpressionResult}}, the second sends and 
removes the property. When the recipient list is the first step of such a 
doFinally, only the evaluation runs, and nothing is sent. A routing slip or 
dynamic router that runs later on the same exchange takes that property as its 
slip ({{RoutingSlip.process}}), so it sends to the recipient list's endpoints 
and not to its own:
{code:java}
errorHandler(deadLetterChannel("mock:dead"));
from("direct:r0").doTry().to("direct:r2").doFinally().routingSlip(constant("mock:slip")).end();
from("direct:r2").doTry().to("direct:r3").doFinally().recipientList(constant("mock:a,mock:b")).end();
from("direct:r3").throwException(new IOException("r3 failed"));

-> mock:slip 0, mock:a 1 (sent by the routing slip), mock:b 0
{code}
When the stale value is not a valid endpoint list for the routing slip's 
delimiter (a recipient list with {{delimiter(";")}}), the routing slip sends to 
one {{direct:}} endpoint without a consumer and blocks for the direct timeout 
(30 s). The same happens with {{onException(...).handled(true)}} instead of the 
dead letter channel, and with a dynamic router.

CAMEL-19441 (Not A Bug) decided that after a called route handled the failure, 
the caller does not continue routing after the {{doTry}}. That stays true with 
the fix ({{mock:end}} stays 0); this issue is only about the {{doFinally}} 
block, which today runs exactly one step.

*Fix* (proposed): {{FinallyProcessor}} clears both marks while the 
{{doFinally}} block runs (as {{OnCompletionProcessor}} does for an 
onCompletion), and sets them again when the block is done (the handled mark 
only if the block did not set it itself), so every step runs, a route that the 
block calls handles or continues its own failure as usual, and the route then 
stops after the {{doTry}} as before. The recipient list in the {{doFinally}} 
then sends and removes its property, so nothing stale is left for a routing 
slip. No upgrade guide entry: a {{doFinally}} that now runs all its steps is 
the fix. Routes that relied on the old behaviour see the second and later steps 
of such a {{doFinally}} run (and a recipient list or routing slip there send to 
its endpoints), once per exchange.

Not changed: {{RoutingSlip.process}} still reads 
{{CamelEvaluateExpressionResult}}, because camel-bean's {{@RoutingSlip}} and 
{{@DynamicRouter}} annotations hand their result to the routing slip in that 
property ({{MethodInfo}}; {{RoutingSlipPOJOTest}}). A dedicated key for that 
hand-off ("TODO: Maybe use a new constant than EVALUATE_EXPRESSION_RESULT", 
from CAMEL-13449) would stop a routing slip from reading another EIP's value at 
all; it touches camel-bean and can be a separate minor issue.

*Tests*: {{DoFinallyAfterHandledCalledRouteTest}} (6 tests: dead letter 
channel, the same with asynchronous delayed redelivery, handled 
{{onException}}, a failing route called from the doFinally that its dead letter 
channel handles, one that its onException continues, and a control where the 
called route does not fail; the bug tests also check that the caller does not 
continue after the {{doTry}}, CAMEL-19441) and 
{{RoutingSlipInDoFinallyAfterRecipientListTest}} (5 tests: routing slip with a 
dead letter channel and with a handled {{onException}}, dynamic router, the 
blocking case with a delimiter, and a control). The 9 non-control tests fail on 
main and pass with the fix; with a fix that does not set the handled mark again 
after the block, three {{doFinally}} tests fail on {{mock:end}}, and with a fix 
that clears only the handled mark, the two tests with a failing route called 
from the doFinally fail. The doTry/doCatch/doFinally, onException, redelivery, 
dead letter channel, error handler, routing slip, recipient list, dynamic 
router and pipeline tests of camel-core pass (984 tests with the onCompletion 
tests, including {{RoutingSlipPOJOTest}}); the camel-core suite (the run was 
stopped by a resource guard of the machine after 2951 of about 3030 test 
classes) has only the known failures of the local offline build.

*How it was found*: a Rocq specification of Camel's error handling, written 
from the documentation, was compared with real Camel on generated routes, then 
reproduced on HotSpot.

*Affected*: main, 4.23.0, and all of camel-4.22.x, 4.18.x and 4.14.x 
({{FinallyProcessor}}, {{Pipeline}} and {{PipelineHelper}} have the same code 
there).

_Filed with Claude Code on behalf of allthingssecurity._



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

Reply via email to