1use crate::do_try::CatchMatcher;
7use crate::error_handler::record_span_error;
8use camel_api::error_handler::ExceptionDisposition;
9use camel_api::outcome_pipeline::OutcomePipeline;
10use camel_api::pipeline_outcome::PipelineOutcome;
11use camel_api::{CamelError, Exchange, FilterPredicate};
12use std::future::Future;
13use std::pin::Pin;
14
15#[derive(Clone)]
19pub struct CatchClauseSegment {
20 pub matcher: CatchMatcher,
21 pub on_when: Option<FilterPredicate>,
22 pub body: camel_api::OutcomeSegment,
23 pub disposition: ExceptionDisposition,
24}
25
26#[derive(Clone)]
28pub struct FinallyClauseSegment {
29 pub on_when: Option<FilterPredicate>,
30 pub body: camel_api::OutcomeSegment,
31}
32
33pub struct DoTrySegment {
39 pub try_body: camel_api::OutcomeSegment,
40 pub catches: Vec<CatchClauseSegment>,
41 pub finally: Option<FinallyClauseSegment>,
42}
43
44impl Clone for DoTrySegment {
45 fn clone(&self) -> Self {
46 Self {
47 try_body: self.try_body.clone(),
48 catches: self.catches.clone(),
49 finally: self.finally.clone(),
50 }
51 }
52}
53
54enum FinallyOutcome {
59 Stopped(Box<Exchange>),
60 Failed(CamelError),
61}
62
63#[allow(clippy::result_large_err)]
70async fn run_finally_body(
71 finally: &mut Option<FinallyClauseSegment>,
72 ex: Exchange,
73) -> Result<Exchange, FinallyOutcome> {
74 let Some(f) = finally.as_mut() else {
75 return Ok(ex);
76 };
77 if !f.on_when.as_ref().map(|p| p(&ex)).unwrap_or(true) {
78 return Ok(ex);
79 }
80 match f.body.run(ex).await {
81 PipelineOutcome::Completed(e) => Ok(e),
82 PipelineOutcome::Stopped(e) => Err(FinallyOutcome::Stopped(Box::new(e))),
83 PipelineOutcome::Failed(e) => Err(FinallyOutcome::Failed(e)),
84 }
85}
86
87impl OutcomePipeline for DoTrySegment {
88 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
89 Box::new(self.clone())
90 }
91
92 fn run<'a>(
93 &'a mut self,
94 exchange: Exchange,
95 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
96 Box::pin(async move {
97 let exchange_for_unmatched = exchange.clone();
100
101 let try_outcome = self.try_body.run(exchange).await;
103 let returned_ex = match try_outcome {
104 PipelineOutcome::Stopped(ex) => return PipelineOutcome::Stopped(ex),
105 PipelineOutcome::Completed(ex) => ex,
106 PipelineOutcome::Failed(err) => {
107 let mut current_ex = exchange_for_unmatched;
111 current_ex.set_error(err.clone());
112 for catch in self.catches.iter_mut() {
113 if catch.matcher.matches(&err, ¤t_ex)
114 && catch
115 .on_when
116 .as_ref()
117 .map(|p| p(¤t_ex))
118 .unwrap_or(true)
119 {
120 match catch.body.run(current_ex).await {
121 PipelineOutcome::Stopped(stopped_ex) => {
123 return PipelineOutcome::Stopped(stopped_ex);
124 }
125 PipelineOutcome::Completed(next) => {
127 match catch.disposition {
128 ExceptionDisposition::Handled => {
129 current_ex = next;
130 break;
131 }
132 ExceptionDisposition::Continued => {
133 current_ex = next;
134 break;
135 }
136 _ => {
139 match run_finally_body(&mut self.finally, next).await {
140 Ok(_) => {}
141 Err(FinallyOutcome::Stopped(e)) => {
142 return PipelineOutcome::Stopped(*e);
143 }
144 Err(FinallyOutcome::Failed(_finally_err)) => {
145 tracing::warn!(
146 error = %err,
147 "doFinally threw during Propagate; \
148 restoring original"
149 );
150 return PipelineOutcome::Failed(err);
151 }
152 }
153 return PipelineOutcome::Failed(err);
154 }
155 }
156 }
157 PipelineOutcome::Failed(catch_err) => {
164 tracing::warn!(
171 original_error = %err,
172 catch_error = %catch_err,
173 "do_try catch block failed; catch error supersedes original"
174 );
175 record_span_error(&catch_err);
176 return PipelineOutcome::Failed(catch_err);
177 }
178 }
179 }
180 }
181 current_ex
182 }
183 };
184 match run_finally_body(&mut self.finally, returned_ex).await {
187 Ok(ex) => PipelineOutcome::Completed(ex),
188 Err(FinallyOutcome::Stopped(e)) => PipelineOutcome::Stopped(*e),
189 Err(FinallyOutcome::Failed(finally_err)) => {
190 tracing::warn!(
191 error = %finally_err,
192 "doFinally threw during/after catch; surfacing finally error"
193 );
194 PipelineOutcome::Failed(finally_err)
195 }
196 }
197 })
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::do_try::CatchMatcher;
205 use crate::test_log_capture::{capture_debugs_with_span_records, captured_field, record_field};
206 use camel_api::pipeline_outcome::PipelineOutcome;
207 use std::sync::Arc;
208 use std::sync::atomic::{AtomicU32, Ordering};
209
210 struct CompleteSegment;
213
214 impl OutcomePipeline for CompleteSegment {
215 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
216 Box::new(CompleteSegment)
217 }
218 fn run<'a>(
219 &'a mut self,
220 exchange: Exchange,
221 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
222 Box::pin(async move { PipelineOutcome::Completed(exchange) })
223 }
224 }
225
226 fn seg_complete() -> camel_api::OutcomeSegment {
227 camel_api::OutcomeSegment::new(Box::new(CompleteSegment))
228 }
229
230 struct FailSegment(CamelError);
231
232 impl OutcomePipeline for FailSegment {
233 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
234 Box::new(FailSegment(self.0.clone()))
235 }
236 fn run<'a>(
237 &'a mut self,
238 _exchange: Exchange,
239 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
240 let e = self.0.clone();
241 Box::pin(async move { PipelineOutcome::Failed(e) })
242 }
243 }
244
245 fn seg_fail(err: CamelError) -> camel_api::OutcomeSegment {
246 camel_api::OutcomeSegment::new(Box::new(FailSegment(err)))
247 }
248
249 struct MutateThenStop {
250 mutator: Arc<dyn Fn(&mut Exchange) + Send + Sync>,
251 }
252
253 impl OutcomePipeline for MutateThenStop {
254 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
255 Box::new(MutateThenStop {
256 mutator: Arc::clone(&self.mutator),
257 })
258 }
259 fn run<'a>(
260 &'a mut self,
261 mut exchange: Exchange,
262 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
263 let m = Arc::clone(&self.mutator);
264 Box::pin(async move {
265 m(&mut exchange);
266 PipelineOutcome::Stopped(exchange)
267 })
268 }
269 }
270
271 fn seg_stop_with(
272 mutator: impl Fn(&mut Exchange) + Send + Sync + 'static,
273 ) -> camel_api::OutcomeSegment {
274 camel_api::OutcomeSegment::new(Box::new(MutateThenStop {
275 mutator: Arc::new(mutator),
276 }))
277 }
278
279 struct RecordCall {
280 counter: Arc<AtomicU32>,
281 }
282
283 impl OutcomePipeline for RecordCall {
284 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
285 Box::new(RecordCall {
286 counter: Arc::clone(&self.counter),
287 })
288 }
289 fn run<'a>(
290 &'a mut self,
291 exchange: Exchange,
292 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
293 let c = Arc::clone(&self.counter);
294 Box::pin(async move {
295 c.fetch_add(1, Ordering::SeqCst);
296 PipelineOutcome::Completed(exchange)
297 })
298 }
299 }
300
301 fn seg_record(counter: Arc<AtomicU32>) -> camel_api::OutcomeSegment {
302 camel_api::OutcomeSegment::new(Box::new(RecordCall { counter }))
303 }
304
305 #[tokio::test]
308 async fn stop_inside_try_skips_catch_and_finally() {
309 let catch_call = Arc::new(AtomicU32::new(0));
310 let finally_call = Arc::new(AtomicU32::new(0));
311
312 let mut seg = DoTrySegment {
313 try_body: seg_stop_with(|ex| {
314 ex.set_property("mutated", camel_api::Value::Bool(true));
315 }),
316 catches: vec![CatchClauseSegment {
317 matcher: CatchMatcher::ByVariant(vec!["*".into()]),
318 on_when: None,
319 body: seg_record(catch_call.clone()),
320 disposition: ExceptionDisposition::Handled,
321 }],
322 finally: Some(FinallyClauseSegment {
323 on_when: None,
324 body: seg_record(finally_call.clone()),
325 }),
326 };
327
328 let result = seg.run(Exchange::default()).await;
329 match result {
330 PipelineOutcome::Stopped(ex) => {
331 assert_eq!(
332 ex.properties.get("mutated"),
333 Some(&camel_api::Value::Bool(true)),
334 "try body mutation must be preserved in Stopped exchange"
335 );
336 }
337 other => panic!("expected Stopped, got {:?}", other),
338 }
339 assert_eq!(
340 catch_call.load(Ordering::SeqCst),
341 0,
342 "catch must NOT run when try stops"
343 );
344 assert_eq!(
345 finally_call.load(Ordering::SeqCst),
346 0,
347 "finally must NOT run when try stops"
348 );
349 }
350
351 #[tokio::test]
352 async fn stop_inside_catch_skips_finally() {
353 let finally_call = Arc::new(AtomicU32::new(0));
354
355 let mut seg = DoTrySegment {
356 try_body: seg_fail(CamelError::ProcessorError("boom".into())),
357 catches: vec![CatchClauseSegment {
358 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
359 on_when: None,
360 body: seg_stop_with(|ex| {
361 ex.set_property("catch_mutated", camel_api::Value::Bool(true));
362 }),
363 disposition: ExceptionDisposition::Handled,
364 }],
365 finally: Some(FinallyClauseSegment {
366 on_when: None,
367 body: seg_record(finally_call.clone()),
368 }),
369 };
370
371 let result = seg.run(Exchange::default()).await;
372 match result {
373 PipelineOutcome::Stopped(ex) => {
374 assert_eq!(
375 ex.properties.get("catch_mutated"),
376 Some(&camel_api::Value::Bool(true)),
377 "catch body mutation must be preserved in Stopped exchange"
378 );
379 }
380 other => panic!("expected Stopped, got {:?}", other),
381 }
382 assert_eq!(
383 finally_call.load(Ordering::SeqCst),
384 0,
385 "finally must NOT run when catch stops"
386 );
387 }
388
389 #[tokio::test]
390 async fn stop_inside_finally_stops_outer_route() {
391 let mut seg = DoTrySegment {
392 try_body: seg_complete(),
393 catches: vec![],
394 finally: Some(FinallyClauseSegment {
395 on_when: None,
396 body: seg_stop_with(|ex| {
397 ex.set_property("finally_mutated", camel_api::Value::Bool(true));
398 }),
399 }),
400 };
401
402 let result = seg.run(Exchange::default()).await;
403 match result {
404 PipelineOutcome::Stopped(ex) => {
405 assert_eq!(
406 ex.properties.get("finally_mutated"),
407 Some(&camel_api::Value::Bool(true)),
408 "finally body mutation must be preserved in Stopped exchange"
409 );
410 }
411 other => panic!("expected Stopped, got {:?}", other),
412 }
413 }
414
415 #[tokio::test]
416 async fn catch_on_when_false_falls_through_to_next_catch() {
417 let first_call = Arc::new(AtomicU32::new(0));
418 let second_call = Arc::new(AtomicU32::new(0));
419
420 let mut seg = DoTrySegment {
421 try_body: seg_fail(CamelError::Io("disk err".into())),
422 catches: vec![
423 CatchClauseSegment {
424 matcher: CatchMatcher::ByVariant(vec!["Io".into()]),
425 on_when: Some(FilterPredicate::new(|_ex| false)),
426 body: seg_record(first_call.clone()),
427 disposition: ExceptionDisposition::Handled,
428 },
429 CatchClauseSegment {
430 matcher: CatchMatcher::ByVariant(vec!["*".into()]),
431 on_when: None,
432 body: seg_record(second_call.clone()),
433 disposition: ExceptionDisposition::Handled,
434 },
435 ],
436 finally: None,
437 };
438
439 let result = seg.run(Exchange::default()).await;
440 assert!(
441 matches!(result, PipelineOutcome::Completed(_)),
442 "expected Completed after second catch"
443 );
444 assert_eq!(
445 first_call.load(Ordering::SeqCst),
446 0,
447 "first catch must NOT fire (on_when=false)"
448 );
449 assert_eq!(
450 second_call.load(Ordering::SeqCst),
451 1,
452 "second catch must fire"
453 );
454 }
455
456 #[tokio::test]
457 async fn finally_on_when_false_skips_finally_entirely() {
458 let finally_call = Arc::new(AtomicU32::new(0));
459 let mut ex = Exchange::default();
460 ex.set_property("try_set", camel_api::Value::Bool(true));
461
462 let mut seg = DoTrySegment {
463 try_body: seg_complete(),
464 catches: vec![],
465 finally: Some(FinallyClauseSegment {
466 on_when: Some(FilterPredicate::new(|_ex| false)),
467 body: seg_record(finally_call.clone()),
468 }),
469 };
470
471 let result = seg.run(ex).await;
472 match result {
473 PipelineOutcome::Completed(ex) => {
474 assert_eq!(
475 ex.properties.get("try_set"),
476 Some(&camel_api::Value::Bool(true)),
477 "exchange state from try must be preserved"
478 );
479 }
480 other => panic!("expected Completed, got {:?}", other),
481 }
482 assert_eq!(
483 finally_call.load(Ordering::SeqCst),
484 0,
485 "finally must NOT run when on_when=false"
486 );
487 }
488
489 fn catch_fails_segment() -> DoTrySegment {
492 DoTrySegment {
493 try_body: seg_fail(CamelError::ProcessorError("orig".into())),
494 catches: vec![CatchClauseSegment {
495 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
496 on_when: None,
497 body: seg_fail(CamelError::Io("catch-fail".into())),
498 disposition: ExceptionDisposition::Handled,
499 }],
500 finally: None,
501 }
502 }
503
504 #[test]
505 fn catch_body_failure_returns_catch_err_and_marks_span() {
506 let mut seg = catch_fails_segment();
507 let (result, _captured, span_records) = capture_debugs_with_span_records(|| {
508 let span = tracing::info_span!("dotry_seg_test", error = tracing::field::Empty);
510 let _guard = span.enter();
511 tokio::runtime::Builder::new_current_thread()
512 .enable_all()
513 .build()
514 .expect("current-thread runtime")
515 .block_on(async { seg.run(Exchange::default()).await })
516 });
517
518 assert!(
519 matches!(result, PipelineOutcome::Failed(CamelError::Io(_))),
520 "catch failure must surface the catch error as Failed(Io), got: {result:?}"
521 );
522 assert!(
526 span_records.iter().any(|line| {
527 captured_field(line, "error").is_some_and(|v| v.contains("catch-fail"))
528 }),
529 "expected span error record carrying the catch error, span records: {span_records:?}"
530 );
531 }
532
533 #[test]
534 fn catch_body_failure_emits_envelope_log() {
535 let mut seg = catch_fails_segment();
536 let (result, captured, _span_records) = capture_debugs_with_span_records(|| {
537 tokio::runtime::Builder::new_current_thread()
538 .enable_all()
539 .build()
540 .expect("current-thread runtime")
541 .block_on(async { seg.run(Exchange::default()).await })
542 });
543
544 assert!(
545 matches!(result, PipelineOutcome::Failed(CamelError::Io(_))),
546 "catch failure must surface the catch error as Failed(Io), got: {result:?}"
547 );
548 let original = record_field(&captured, "do_try catch block failed", "original_error")
551 .unwrap_or_else(|| {
552 panic!("envelope record missing original_error field, captured: {captured:?}")
553 });
554 let catch = record_field(&captured, "do_try catch block failed", "catch_error")
555 .unwrap_or_else(|| {
556 panic!("envelope record missing catch_error field, captured: {captured:?}")
557 });
558 assert!(
559 original.contains("orig"),
560 "original_error must carry the original error, got: {original}"
561 );
562 assert!(
563 catch.contains("catch-fail"),
564 "catch_error must carry the catch error, got: {catch}"
565 );
566 }
567}