[ 
https://issues.apache.org/jira/browse/CAMEL-25577?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Claus Ibsen updated CAMEL-25577:
--------------------------------
    Fix Version/s: 4.24.0

> camel-telegram - when Telegram answers with an error status or an unreadable 
> body, the route first continues as if the message was sent and then the 
> exchange fails too (completed twice)
> -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: CAMEL-25577
>                 URL: https://issues.apache.org/jira/browse/CAMEL-25577
>             Project: Camel
>          Issue Type: Bug
>          Components: camel-telegram
>            Reporter: shashank
>            Assignee: shashank
>            Priority: Major
>             Fix For: 4.24.0
>
>
> {{TelegramProducer.process}} returns {{false}} (line 84 at main c3829069cb72) 
> and leaves the completion of the exchange to 
> {{TelegramMessageHandler.sendMessage}} (lines 70-71):
> {code:java}
> apiClient.client().sendAsync(builder.build(), 
> HttpResponse.BodyHandlers.ofInputStream())
>         .thenApply(telegramAsyncHandler::handleCompressedResponse).get();
> {code}
> {{TelegramAsyncHandler.handleCompressedResponse}} (lines 81-120) throws a 
> {{RuntimeCamelException}} for a status outside 2xx (line 108) and for a body 
> it cannot parse (line 114), and calls {{callback.done(false)}} in a 
> {{finally}} block (lines 115-117). The exception is not set on the exchange; 
> it only completes the future exceptionally. So for such a response:
> # the {{finally}} block calls the callback while the exchange has no 
> exception: the route continues after the producer as if the message had been 
> sent (on the thread of the HTTP client);
> # {{get()}} throws {{ExecutionException}}, {{sendMessage}} rethrows it as 
> {{RuntimeException}} (line 76) out of {{process()}};
> # the caller of the producer catches it, sets it on the exchange and calls 
> the callback again ({{SendProcessor.sendUsingProducer}}, lines 273-276).
> The exchange is completed twice, once as a success and once as a failure. 
> With {{from(...).to("telegram:bots").to("jms:sent")}} a message that Telegram 
> rejected (400 for an unknown chat, 403 when the bot was blocked, 429 when 
> rate limited, a 502 page of a proxy) is reported to {{jms:sent}} as sent. The 
> exception that {{process()}} throws afterwards is a {{RuntimeException}} that 
> wraps an {{ExecutionException}} that wraps the {{RuntimeCamelException}}; 
> without an {{onException}} it is on the exchange that the caller of the route 
> gets back.
> The failure arrives after the route has finished, so the error handling of 
> the route does not see it properly. Tested on main with a route-scoped 
> {{onException(Exception.class).handled(true).to("mock:failed")}} in front of 
> the producer: {{mock:after}} gets the exchange, {{mock:failed}} gets nothing, 
> and {{RedeliveryErrorHandler}} logs {{Cannot determine current route from 
> Exchange with id: ..., will fallback and use first error handler.}} I tested 
> this one case only (no redelivery, no context-scoped {{onException}}).
> A request that fails without a response (connection refused) is handled 
> correctly on main: {{thenApply}} is skipped, so only the caller completes the 
> exchange.
> h3. Reproduction
> New {{TelegramProducerFailureTest}} (6 tests) with the mock Bot API of the 
> module ({{TelegramMockRoutes}}); all six fail with the two classes of main:
> * {{errorStatusFailsTheExchangeOnce}} ({{sendMessage}} answers 400) and 
> {{unreadableAnswerFailsTheExchangeOnce}} (200 with an HTML body) send through 
> {{direct:telegram -> telegram:bots -> mock:after}} and with the producer and 
> a callback that records each call: {{[exchanges at the step after the 
> producer] expected: 0 but was: 1}}, {{[exchange failed when it was completed] 
> expected: true but was: false}}, {{[thrown by process() of the producer] 
> expected: null but was: java.lang.RuntimeException: 
> java.util.concurrent.ExecutionException: 
> org.apache.camel.RuntimeCamelException: ... responded: 400...}}.
> * {{errorStatusIsHandledByOnExceptionAndTheRouteDoesNotContinue}}: 
> {{[exchanges at the step after the producer] expected: 0 but was: 1}} and 
> {{[exchanges at onException] expected: 1 but was: 0}}.
> * {{requestWithoutAnswerFailsTheExchangeOnce}} ({{baseUri}} with a port 
> nothing listens on) and {{interruptedCallerFailsTheExchangeOnce}} fail on 
> main only because {{process()}} throws instead of completing the exchange 
> through the callback; they cover the new handling of these two cases.
> * {{twoSendsAreNotInterleavedAndSetTheResult}} (the mock API holds the first 
> answer back for up to a second): the order of the requests and the 
> {{MessageResult}} body are the same on main; it fails there only on the 
> thread that calls the callback (a thread of the HTTP client on main).
> h3. Proposed fix
> Keep the wait, fix the completion. {{handleCompressedResponse}} no longer 
> calls the callback. {{sendMessage}} still waits in {{get()}} on the calling 
> thread, then sets the failure on the exchange and calls 
> {{callback.done(false)}} once ({{process()}} returns {{false}}, as before): 
> for an answer, for a request that failed without one, and when the wait was 
> interrupted.
> The blocking {{get()}} stays on purpose. It came with CAMEL-18506; removing 
> it would let asynchronous callers (a seda consumer, for example) send 
> concurrently, the order of the messages of one caller would no longer be 
> kept, and a 429 would become more likely. That would be a separate change.
> Behaviour change, with a 4.24 upgrade-guide entry:
> * a rejected message fails the exchange once and the route stops at the 
> producer;
> * the exception of the exchange is the same as before ({{RuntimeException}} 
> -> {{ExecutionException}} -> {{RuntimeCamelException}} or {{IOException}}); 
> on main {{process()}} threw it, now the producer sets it on the exchange;
> * the steps after the producer run on the calling thread. On main they ran on 
> a thread of the HTTP client after a successful answer, while the calling 
> thread was blocked in {{get()}}.
> Not changed: the request has no timeout (a server that accepts the connection 
> and never answers leaves the calling thread waiting, as on main).
> camel-telegram tests with the fix: 99 run, 0 failures, 1 skipped (the 
> integration tests need a bot token and were not run).
> How it was found: a small TLA+ model of one send (request, outcome in 
> \{response 2xx, error status, unreadable body, no response\}, the handler 
> stage, the blocked caller). For the code of main TLC reports a violation of 
> {{NoFalseSuccess}} (3 states: Send, Handle: callback without exception) and 
> of {{CompletedOnce}} (4 states: then CallerGet: callback again) for an error 
> status and for an unreadable body; a 2xx response and a request without a 
> response hold. The model is a sketch of the code, not a proof of the fix; the 
> same outcome matrix was then reproduced on the real producer with the test 
> above.
> Affected: main (c3829069cb72; its two classes were tested), camel-4.23.x 
> (197e70656a41: the same code at the same lines, {{TelegramAsyncHandler}} 
> 108-117 and {{TelegramMessageHandler}} 70-77; read, not run), camel-4.22.x 
> and camel-4.18.x (the same two lines; read, not run). camel-4.14.x has a 
> different class layout and was not checked.
> Duplicate check (2026-10-12): JIRA text "TelegramAsyncHandler" (none), 
> "telegram producer callback" (none), "telegram ExecutionException" (none), 
> "telegram responded" (11, none about the completion of the exchange; closest 
> CAMEL-13903 and CAMEL-18506, the two HTTP client changes); GitHub pull 
> requests "telegram producer" in title and "TelegramAsyncHandler": none.
> _Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to