1use camel_api::error_handler::ExceptionDisposition;
9use camel_api::exchange::PROPERTY_EXCEPTION_HANDLED;
10use camel_api::{BoxProcessor, CamelError, Exchange, PredicateSource};
11use tower::Service;
12use tower::ServiceExt;
13
14use crate::error_handler::record_span_error;
15
16#[derive(Clone)]
18pub enum CatchMatcher {
19 ByVariant(Vec<String>),
22 Predicate(PredicateSource),
24}
25
26impl CatchMatcher {
27 pub async fn matches(&self, err: &CamelError, ex: &Exchange) -> Result<bool, CamelError> {
32 match self {
33 CatchMatcher::ByVariant(names) => {
34 if names.iter().any(|n| n == "*") {
35 return Ok(true);
36 }
37 Ok(names.iter().any(|n| n == err.variant_name()))
38 }
39 CatchMatcher::Predicate(source) => source.matches(ex).await,
40 }
41 }
42}
43
44pub(crate) fn chain_predicate_error(
56 mut predicate_err: CamelError,
57 original: CamelError,
58) -> CamelError {
59 let should_chain = match &predicate_err {
60 CamelError::ExpressionFailed { cause, .. } => {
61 cause.is_none() || camel_language_api::is_expression_detail_cause(&predicate_err)
62 }
63 _ => false,
64 };
65 if should_chain && let CamelError::ExpressionFailed { cause, .. } = &mut predicate_err {
66 *cause = Some(Box::new(original));
67 }
68 predicate_err
69}
70
71#[derive(Clone)]
73pub struct CatchClause {
74 pub matcher: CatchMatcher,
76 pub on_when: Option<PredicateSource>,
78 pub steps: Vec<BoxProcessor>,
80 pub disposition: ExceptionDisposition,
83}
84
85#[derive(Clone)]
87pub struct DoTryService {
88 pub try_steps: Vec<BoxProcessor>,
90 pub catch_clauses: Vec<CatchClause>,
92 pub finally_steps: Vec<BoxProcessor>,
94 pub finally_on_when: Option<PredicateSource>,
96}
97
98impl DoTryService {
99 pub fn new(try_steps: Vec<BoxProcessor>) -> Self {
101 Self {
102 try_steps,
103 catch_clauses: Vec::new(),
104 finally_steps: Vec::new(),
105 finally_on_when: None,
106 }
107 }
108
109 pub fn with_catch_and_finally(
112 try_steps: Vec<BoxProcessor>,
113 catch_clauses: Vec<CatchClause>,
114 finally_steps: Vec<BoxProcessor>,
115 finally_on_when: Option<PredicateSource>,
116 ) -> Self {
117 Self {
118 try_steps,
119 catch_clauses,
120 finally_steps,
121 finally_on_when,
122 }
123 }
124}
125
126async fn run_pipeline(
129 steps: Vec<BoxProcessor>,
130 mut ex: Exchange,
131) -> Result<Exchange, Box<(Exchange, CamelError)>> {
132 for mut svc in steps {
133 match svc.ready().await {
134 Ok(ready) => {
135 let snapshot = ex.branch_clone();
136 match ready.call(ex).await {
137 Ok(new_ex) => ex = new_ex,
138 Err(err) => return Err(Box::new((snapshot, err))),
139 }
140 }
141 Err(err) => return Err(Box::new((ex, err))),
142 }
143 }
144 Ok(ex)
145}
146
147async fn run_finally(
159 finally_steps: Vec<BoxProcessor>,
160 finally_on_when: Option<PredicateSource>,
161 ex: Exchange,
162 previous_err: Option<CamelError>,
163) -> Result<FinallyOutcome, CamelError> {
164 if finally_steps.is_empty() {
165 return Ok(FinallyOutcome::Completed(ex));
166 }
167 if let Some(on_when) = &finally_on_when
168 && !on_when.matches(&ex).await?
169 {
170 return Ok(FinallyOutcome::Completed(ex));
171 }
172 match run_pipeline(finally_steps, ex).await {
173 Ok(ex) => Ok(FinallyOutcome::Completed(ex)),
174 Err(failed) => {
175 let (_, finally_err) = *failed;
176 Ok(match previous_err {
177 Some(prev) => FinallyOutcome::Restore {
178 previous: prev,
179 finally_err,
180 },
181 None => FinallyOutcome::NoPreviousFail(finally_err),
182 })
183 }
184 }
185}
186
187#[allow(clippy::large_enum_variant)]
193enum FinallyOutcome {
194 Completed(Exchange),
196 NoPreviousFail(CamelError),
198 Restore {
200 previous: CamelError,
201 finally_err: CamelError,
202 },
203}
204
205async fn run_finally_for_failure(
211 finally_steps: Vec<BoxProcessor>,
212 finally_on_when: Option<PredicateSource>,
213 ex: Exchange,
214 failure: CamelError,
215) -> CamelError {
216 match run_finally(finally_steps, finally_on_when, ex, Some(failure.clone())).await {
217 Err(pred_err) => pred_err,
218 Ok(FinallyOutcome::Restore {
219 previous,
220 finally_err,
221 }) => {
222 tracing::warn!(
223 finally_error = %finally_err,
224 previous_error = %previous,
225 "doFinally threw after catch predicate failure; restoring previous (Camel parity)"
226 );
227 previous
228 }
229 Ok(_) => failure,
232 }
233}
234
235impl tower::Service<Exchange> for DoTryService {
236 type Response = Exchange;
237 type Error = CamelError;
238 type Future = std::pin::Pin<
239 Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
240 >;
241
242 fn poll_ready(
243 &mut self,
244 _cx: &mut std::task::Context<'_>,
245 ) -> std::task::Poll<Result<(), Self::Error>> {
246 std::task::Poll::Ready(Ok(()))
247 }
248
249 fn call(&mut self, mut exchange: Exchange) -> Self::Future {
250 exchange.properties.remove(PROPERTY_EXCEPTION_HANDLED);
253
254 let try_steps = self.try_steps.clone();
255 let catch_clauses = self.catch_clauses.clone();
256 let finally_steps = self.finally_steps.clone();
257 let finally_on_when = self.finally_on_when.clone();
258
259 Box::pin(async move {
260 let try_result = run_pipeline(try_steps, exchange).await;
261 match try_result {
262 Ok(ex) => match run_finally(finally_steps, finally_on_when, ex, None).await {
263 Ok(FinallyOutcome::Completed(ex)) => Ok(ex),
264 Ok(FinallyOutcome::NoPreviousFail(fin)) => {
265 tracing::warn!(error = %fin, "doFinally threw");
266 Err(fin)
267 }
268 Ok(FinallyOutcome::Restore { previous, .. }) => Err(previous),
270 Err(pred_err) => Err(pred_err),
273 },
274 Err(failed) => {
275 let (failed_ex, original_err) = *failed;
276 let mut ex = failed_ex;
277 ex.set_error(original_err.clone());
278
279 for clause in catch_clauses {
280 let CatchClause {
281 matcher,
282 on_when,
283 steps,
284 disposition,
285 } = clause;
286 let matched = match matcher.matches(&original_err, &ex).await {
287 Ok(m) => m,
288 Err(pred_err) => {
292 let chained = chain_predicate_error(pred_err, original_err);
293 return Err(run_finally_for_failure(
294 finally_steps,
295 finally_on_when,
296 ex,
297 chained,
298 )
299 .await);
300 }
301 };
302 if !matched {
303 continue;
304 }
305 if let Some(ref on_when) = on_when {
306 match on_when.matches(&ex).await {
307 Ok(true) => {}
308 Ok(false) => continue,
309 Err(pred_err) => {
312 let chained = chain_predicate_error(pred_err, original_err);
313 return Err(run_finally_for_failure(
314 finally_steps,
315 finally_on_when,
316 ex,
317 chained,
318 )
319 .await);
320 }
321 }
322 }
323
324 let catch_result = run_pipeline(steps, ex.branch_clone()).await;
325
326 return match catch_result {
327 Ok(ok_ex) => {
328 let prev = match disposition {
340 ExceptionDisposition::Handled => None,
341 ExceptionDisposition::Continued => {
342 tracing::warn!(
343 "ExceptionDisposition::Continued reached doTry runtime; \
344 treating as Propagate. Should have been rejected at parse time."
345 );
346 Some(original_err.clone())
347 }
348 _ => Some(original_err.clone()),
350 };
351 let mut ex = match run_finally(
352 finally_steps.clone(),
353 finally_on_when.clone(),
354 ok_ex,
355 prev,
356 )
357 .await
358 {
359 Ok(FinallyOutcome::Completed(ex)) => ex,
360 Ok(FinallyOutcome::NoPreviousFail(fin)) => {
361 tracing::warn!(error = %fin, "doFinally threw");
362 return Err(fin);
363 }
364 Ok(FinallyOutcome::Restore {
365 previous,
366 finally_err,
367 }) => {
368 tracing::warn!(
369 finally_error = %finally_err,
370 previous_error = %previous,
371 "doFinally threw; restoring previous exception (Camel parity)"
372 );
373 return Err(previous);
374 }
375 Err(pred_err) => return Err(pred_err),
378 };
379 if matches!(disposition, ExceptionDisposition::Handled) {
383 ex.handle_error();
384 }
385 match disposition {
386 ExceptionDisposition::Handled => Ok(ex),
387 _ => Err(original_err),
388 }
389 }
390 Err(failed) => {
391 let (catch_ex, catch_err) = *failed;
399 tracing::warn!(
400 original_error = %original_err,
401 catch_error = %catch_err,
402 "do_try catch block failed; catch error supersedes original"
403 );
404 record_span_error(&catch_err);
405 let outcome = run_finally(
409 finally_steps.clone(),
410 finally_on_when.clone(),
411 catch_ex,
412 Some(catch_err.clone()),
413 )
414 .await;
415 match outcome {
416 Err(pred_err) => return Err(pred_err),
417 Ok(FinallyOutcome::Restore {
418 previous,
419 finally_err,
420 }) => {
421 tracing::warn!(
422 catch_error = %previous,
423 finally_error = %finally_err,
424 "doFinally threw after failed catch; restoring catch error"
425 );
426 return Err(previous);
427 }
428 Ok(_) => {}
429 }
430 Err(catch_err)
431 }
432 };
433 }
434
435 match run_finally(
437 finally_steps,
438 finally_on_when,
439 ex,
440 Some(original_err.clone()),
441 )
442 .await
443 {
444 Ok(FinallyOutcome::Restore {
445 previous,
446 finally_err,
447 }) => {
448 tracing::warn!(
449 finally_error = %finally_err,
450 previous_error = %previous,
451 "doFinally threw; restoring previous exception (Camel parity)"
452 );
453 Err(previous)
454 }
455 Ok(_) => Err(original_err),
456 Err(pred_err) => Err(pred_err),
457 }
458 }
459 }
460 })
461 }
462}
463
464#[cfg(test)]
470mod tests {
471 use super::*;
472 use crate::test_log_capture::{capture_debugs_with_span_records, captured_field, record_field};
473 use camel_api::{BoxProcessor, BoxProcessorExt};
474 use std::sync::Arc;
475 use std::sync::atomic::{AtomicU32, Ordering};
476
477 fn passthrough() -> BoxProcessor {
478 BoxProcessor::from_fn(move |ex| Box::pin(async move { Ok(ex) }))
479 }
480
481 fn record_call(flag: Arc<AtomicU32>) -> BoxProcessor {
482 BoxProcessor::from_fn(move |ex| {
483 let f = flag.clone();
484 Box::pin(async move {
485 f.fetch_add(1, Ordering::SeqCst);
486 Ok(ex)
487 })
488 })
489 }
490
491 fn always_fail(err: CamelError) -> BoxProcessor {
492 BoxProcessor::from_fn(move |_ex| {
493 let e = err.clone();
494 Box::pin(async move { Err(e) })
495 })
496 }
497
498 #[tokio::test]
499 async fn happy_path_try_succeeds_finally_runs() {
500 let finally_flag = Arc::new(AtomicU32::new(0));
501 let mut svc = DoTryService::new(vec![passthrough()]);
502 svc.finally_steps = vec![record_call(finally_flag.clone())];
503
504 let mut boxed = BoxProcessor::new(svc);
505 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
506 assert!(result.is_ok());
507 assert_eq!(finally_flag.load(Ordering::SeqCst), 1);
508 }
509
510 #[tokio::test]
511 async fn catch_by_variant_handled_returns_ok() {
512 let try_step = always_fail(CamelError::ProcessorError("boom".into()));
513 let mut svc = DoTryService::new(vec![try_step]);
514 svc.catch_clauses.push(CatchClause {
515 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
516 on_when: None,
517 steps: vec![passthrough()],
518 disposition: ExceptionDisposition::Handled,
519 });
520
521 let mut boxed = BoxProcessor::new(svc);
522 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
523 assert!(result.is_ok(), "Handled must return Ok");
524 let ex = result.unwrap();
525 assert_eq!(
526 ex.properties.get(PROPERTY_EXCEPTION_HANDLED),
527 Some(&camel_api::Value::Bool(true)),
528 "CamelExceptionHandled must be set via handle_error()"
529 );
530 }
531
532 #[tokio::test]
533 async fn catch_by_variant_propagate_runs_side_effects_and_rethrows() {
534 let original = CamelError::ProcessorError("boom".into());
535 let try_step = always_fail(original.clone());
536 let side_effect = Arc::new(AtomicU32::new(0));
537 let catch_step = record_call(side_effect.clone());
538 let mut svc = DoTryService::new(vec![try_step]);
539 svc.catch_clauses.push(CatchClause {
540 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
541 on_when: None,
542 steps: vec![catch_step],
543 disposition: ExceptionDisposition::Propagate,
544 });
545
546 let mut boxed = BoxProcessor::new(svc);
547 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
548 assert!(result.is_err(), "Propagate must rethrow original");
549 assert!(matches!(result.unwrap_err(), CamelError::ProcessorError(_)));
550 assert_eq!(
551 side_effect.load(Ordering::SeqCst),
552 1,
553 "catch branch must have run for side-effects"
554 );
555 }
556
557 #[tokio::test]
558 async fn catch_by_predicate_matches_via_exception_kind() {
559 let try_step = always_fail(CamelError::Io("disk full".into()));
560 let predicate = FilterPredicate::new(|ex: &Exchange| {
561 ex.properties
562 .get(camel_api::exchange::PROPERTY_EXCEPTION_KIND)
563 .map(|v| matches!(v, camel_api::Value::String(s) if s == "io"))
564 .unwrap_or(false)
565 });
566 let mut svc = DoTryService::new(vec![try_step]);
567 svc.catch_clauses.push(CatchClause {
568 matcher: CatchMatcher::Predicate(PredicateSource::Sync(predicate)),
569 on_when: None,
570 steps: vec![passthrough()],
571 disposition: ExceptionDisposition::Handled,
572 });
573
574 let mut boxed = BoxProcessor::new(svc);
575 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
576 assert!(
577 result.is_ok(),
578 "Predicate matcher must catch the error and Handled must return Ok"
579 );
580 }
581
582 #[tokio::test]
583 async fn on_when_filters_clause_and_next_evaluated() {
584 let try_step = always_fail(CamelError::ProcessorError("boom".into()));
585 let first_call = Arc::new(AtomicU32::new(0));
586 let second_call = Arc::new(AtomicU32::new(0));
587
588 let mut svc = DoTryService::new(vec![try_step]);
589 svc.catch_clauses.push(CatchClause {
590 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
591 on_when: Some(PredicateSource::Sync(FilterPredicate::new(|_ex| false))),
592 steps: vec![record_call(first_call.clone())],
593 disposition: ExceptionDisposition::Handled,
594 });
595 svc.catch_clauses.push(CatchClause {
596 matcher: CatchMatcher::ByVariant(vec!["*".into()]),
597 on_when: None,
598 steps: vec![record_call(second_call.clone())],
599 disposition: ExceptionDisposition::Handled,
600 });
601
602 let mut boxed = BoxProcessor::new(svc);
603 let _ = boxed.ready().await.unwrap().call(Exchange::default()).await;
604 assert_eq!(first_call.load(Ordering::SeqCst), 0);
605 assert_eq!(second_call.load(Ordering::SeqCst), 1);
606 }
607
608 #[tokio::test]
609 async fn first_match_wins_subsequent_clauses_not_evaluated() {
610 let try_step = always_fail(CamelError::Io("err".into()));
611 let first_call = Arc::new(AtomicU32::new(0));
612 let second_call = Arc::new(AtomicU32::new(0));
613
614 let mut svc = DoTryService::new(vec![try_step]);
615 svc.catch_clauses.push(CatchClause {
616 matcher: CatchMatcher::ByVariant(vec!["Io".into()]),
617 on_when: None,
618 steps: vec![record_call(first_call.clone())],
619 disposition: ExceptionDisposition::Handled,
620 });
621 svc.catch_clauses.push(CatchClause {
622 matcher: CatchMatcher::ByVariant(vec!["*".into()]),
623 on_when: None,
624 steps: vec![record_call(second_call.clone())],
625 disposition: ExceptionDisposition::Handled,
626 });
627
628 let mut boxed = BoxProcessor::new(svc);
629 let _ = boxed.ready().await.unwrap().call(Exchange::default()).await;
630 assert_eq!(first_call.load(Ordering::SeqCst), 1);
631 assert_eq!(second_call.load(Ordering::SeqCst), 0);
632 }
633
634 #[tokio::test]
635 async fn no_clause_matches_propagates_original() {
636 let try_step = always_fail(CamelError::CircuitOpen("cb".into()));
637 let mut svc = DoTryService::new(vec![try_step]);
638 svc.catch_clauses.push(CatchClause {
639 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
640 on_when: None,
641 steps: vec![passthrough()],
642 disposition: ExceptionDisposition::Handled,
643 });
644
645 let mut boxed = BoxProcessor::new(svc);
646 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
647 assert!(result.is_err());
648 assert!(matches!(result.unwrap_err(), CamelError::CircuitOpen(_)));
649 }
650
651 #[tokio::test]
652 async fn catch_branch_throws_new_error_wins() {
653 let try_step = always_fail(CamelError::ProcessorError("orig".into()));
654 let catch_step = always_fail(CamelError::Io("catch-fail".into()));
655 let mut svc = DoTryService::new(vec![try_step]);
656 svc.catch_clauses.push(CatchClause {
657 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
658 on_when: None,
659 steps: vec![catch_step],
660 disposition: ExceptionDisposition::Handled,
661 });
662
663 let mut boxed = BoxProcessor::new(svc);
664 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
665 assert!(result.is_err());
666 assert!(matches!(result.unwrap_err(), CamelError::Io(_)));
667 }
668
669 #[tokio::test]
670 async fn finally_throws_with_no_previous_error_propagates_finally_error() {
671 let finally_step = always_fail(CamelError::Config("fin".into()));
672 let mut svc = DoTryService::new(vec![passthrough()]);
673 svc.finally_steps = vec![finally_step];
674
675 let mut boxed = BoxProcessor::new(svc);
676 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
677 assert!(result.is_err());
678 assert!(matches!(result.unwrap_err(), CamelError::Config(_)));
679 }
680
681 #[tokio::test]
682 async fn finally_throws_with_previous_error_restores_previous() {
683 let try_step = always_fail(CamelError::ProcessorError("orig".into()));
684 let finally_step = always_fail(CamelError::Config("fin".into()));
685 let mut svc = DoTryService::new(vec![try_step]);
686 svc.finally_steps = vec![finally_step];
687
688 let mut boxed = BoxProcessor::new(svc);
689 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
690 assert!(result.is_err());
691 assert!(
692 matches!(result.unwrap_err(), CamelError::ProcessorError(_)),
693 "previous error must be restored when finally throws (Camel parity)"
694 );
695 }
696
697 #[tokio::test]
698 async fn finally_on_when_false_skips_finally() {
699 let finally_call = Arc::new(AtomicU32::new(0));
700 let mut svc = DoTryService::new(vec![passthrough()]);
701 svc.finally_steps = vec![record_call(finally_call.clone())];
702 svc.finally_on_when = Some(PredicateSource::Sync(FilterPredicate::new(|_ex| false)));
703
704 let mut boxed = BoxProcessor::new(svc);
705 let _ = boxed.ready().await.unwrap().call(Exchange::default()).await;
706 assert_eq!(finally_call.load(Ordering::SeqCst), 0);
707 }
708
709 #[tokio::test]
710 async fn stale_handled_marker_cleared_on_entry() {
711 let mut ex = Exchange::default();
712 ex.set_property(PROPERTY_EXCEPTION_HANDLED, camel_api::Value::Bool(true));
713 let svc = DoTryService::new(vec![passthrough()]);
714 let mut boxed = BoxProcessor::new(svc);
715 let result = boxed.ready().await.unwrap().call(ex).await;
716 let ex = result.unwrap();
717 assert!(
718 !ex.properties.contains_key(PROPERTY_EXCEPTION_HANDLED),
719 "stale CamelExceptionHandled must be cleared on entry"
720 );
721 }
722
723 #[tokio::test]
724 async fn nested_do_try_inner_catch_does_not_leak_to_outer() {
725 let inner = {
726 let try_step = always_fail(CamelError::Io("inner".into()));
727 let mut d = DoTryService::new(vec![try_step]);
728 d.catch_clauses.push(CatchClause {
729 matcher: CatchMatcher::ByVariant(vec!["Io".into()]),
730 on_when: None,
731 steps: vec![passthrough()],
732 disposition: ExceptionDisposition::Handled,
733 });
734 BoxProcessor::new(d)
735 };
736 let mut outer = DoTryService::new(vec![inner]);
737 outer.catch_clauses.push(CatchClause {
738 matcher: CatchMatcher::ByVariant(vec!["Io".into()]),
739 on_when: None,
740 steps: vec![passthrough()],
741 disposition: ExceptionDisposition::Handled,
742 });
743
744 let mut boxed = BoxProcessor::new(outer);
745 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
746 assert!(
747 result.is_ok(),
748 "outer must see Ok because inner handled its own error"
749 );
750 }
751
752 #[tokio::test]
753 async fn catch_all_only_fires_when_no_specific_clause_matches() {
754 let try_step = always_fail(CamelError::Io("err".into()));
755 let processor_call = Arc::new(AtomicU32::new(0));
756 let catch_all_call = Arc::new(AtomicU32::new(0));
757
758 let mut svc = DoTryService::new(vec![try_step]);
759 svc.catch_clauses.push(CatchClause {
761 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
762 on_when: None,
763 steps: vec![record_call(processor_call.clone())],
764 disposition: ExceptionDisposition::Handled,
765 });
766 svc.catch_clauses.push(CatchClause {
768 matcher: CatchMatcher::ByVariant(vec!["*".into()]),
769 on_when: None,
770 steps: vec![record_call(catch_all_call.clone())],
771 disposition: ExceptionDisposition::Handled,
772 });
773
774 let mut boxed = BoxProcessor::new(svc);
775 let _ = boxed.ready().await.unwrap().call(Exchange::default()).await;
776 assert_eq!(
777 processor_call.load(Ordering::SeqCst),
778 0,
779 "specific ProcessorError clause must not fire on Io error"
780 );
781 assert_eq!(
782 catch_all_call.load(Ordering::SeqCst),
783 1,
784 "catch-all clause must fire when no specific clause matches"
785 );
786 }
787
788 #[tokio::test]
789 async fn catch_throws_with_finally_runs_finally_and_propagates_catch_err() {
790 let try_step = always_fail(CamelError::ProcessorError("orig".into()));
791 let catch_step = always_fail(CamelError::Io("catch-fail".into()));
792 let finally_flag = Arc::new(AtomicU32::new(0));
793 let finally_step = record_call(finally_flag.clone());
794
795 let mut svc = DoTryService::new(vec![try_step]);
796 svc.catch_clauses.push(CatchClause {
797 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
798 on_when: None,
799 steps: vec![catch_step],
800 disposition: ExceptionDisposition::Handled,
801 });
802 svc.finally_steps = vec![finally_step];
803
804 let mut boxed = BoxProcessor::new(svc);
805 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
806
807 assert!(result.is_err());
808 assert!(
809 matches!(result.unwrap_err(), CamelError::Io(_)),
810 "catch_err must propagate (not original ProcessorError)"
811 );
812 assert_eq!(
813 finally_flag.load(Ordering::SeqCst),
814 1,
815 "doFinally must run even when catch throws"
816 );
817 }
818
819 #[tokio::test]
820 async fn catch_throws_and_finally_throws_restores_catch_err() {
821 let try_step = always_fail(CamelError::ProcessorError("orig".into()));
822 let catch_step = always_fail(CamelError::Io("catch-fail".into()));
823 let finally_step = always_fail(CamelError::Config("fin-fail".into()));
824
825 let mut svc = DoTryService::new(vec![try_step]);
826 svc.catch_clauses.push(CatchClause {
827 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
828 on_when: None,
829 steps: vec![catch_step],
830 disposition: ExceptionDisposition::Handled,
831 });
832 svc.finally_steps = vec![finally_step];
833
834 let mut boxed = BoxProcessor::new(svc);
835 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
836
837 assert!(result.is_err());
838 assert!(
839 matches!(result.unwrap_err(), CamelError::Io(_)),
840 "catch_err (Io) must be restored over finally_err (Config) per Camel parity"
841 );
842 }
843
844 #[tokio::test]
845 async fn finally_on_when_false_with_previous_error_still_propagates_original() {
846 let try_step = always_fail(CamelError::ProcessorError("orig".into()));
847 let finally_flag = Arc::new(AtomicU32::new(0));
848 let finally_step = record_call(finally_flag.clone());
849
850 let mut svc = DoTryService::new(vec![try_step]);
851 svc.finally_steps = vec![finally_step];
853 svc.finally_on_when = Some(PredicateSource::Sync(FilterPredicate::new(|_ex| false)));
854
855 let mut boxed = BoxProcessor::new(svc);
856 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
857
858 assert!(result.is_err());
859 assert!(
860 matches!(result.unwrap_err(), CamelError::ProcessorError(_)),
861 "original error must propagate even when finally_on_when skips finally"
862 );
863 assert_eq!(
864 finally_flag.load(Ordering::SeqCst),
865 0,
866 "doFinally must NOT run when on_when returns false"
867 );
868 }
869
870 fn catch_fails_service() -> BoxProcessor {
871 let try_step = always_fail(CamelError::ProcessorError("orig-lost".into()));
872 let catch_step = always_fail(CamelError::Io("catch-fail".into()));
873 let mut svc = DoTryService::new(vec![try_step]);
874 svc.catch_clauses.push(CatchClause {
875 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
876 on_when: None,
877 steps: vec![catch_step],
878 disposition: ExceptionDisposition::Handled,
879 });
880 BoxProcessor::new(svc)
881 }
882
883 #[tokio::test]
884 async fn catch_throws_under_propagate_disposition_returns_catch_err() {
885 let try_step = always_fail(CamelError::ProcessorError("orig".into()));
886 let catch_step = always_fail(CamelError::Io("catch-fail".into()));
887 let mut svc = DoTryService::new(vec![try_step]);
888 svc.catch_clauses.push(CatchClause {
889 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
890 on_when: None,
891 steps: vec![catch_step],
892 disposition: ExceptionDisposition::Propagate,
893 });
894
895 let mut boxed = BoxProcessor::new(svc);
896 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
897 assert!(
898 matches!(result, Err(CamelError::Io(_))),
899 "Propagate-disposition catch failure must return the catch error, \
900 same envelope as Handled, got: {result:?}"
901 );
902 }
903
904 #[test]
905 fn catch_throws_logs_original_and_catch_error() {
906 let mut boxed = catch_fails_service();
907 let (result, captured, _span_records) = capture_debugs_with_span_records(|| {
908 tokio::runtime::Builder::new_current_thread()
909 .enable_all()
910 .build()
911 .expect("current-thread runtime")
912 .block_on(async { boxed.ready().await.unwrap().call(Exchange::default()).await })
913 });
914
915 assert!(
916 matches!(result, Err(CamelError::Io(_))),
917 "catch failure must return Err(Io), got: {result:?}"
918 );
919 let original = record_field(&captured, "do_try catch block failed", "original_error")
922 .unwrap_or_else(|| {
923 panic!("envelope record missing original_error field, captured: {captured:?}")
924 });
925 let catch = record_field(&captured, "do_try catch block failed", "catch_error")
926 .unwrap_or_else(|| {
927 panic!("envelope record missing catch_error field, captured: {captured:?}")
928 });
929 assert!(
930 original.contains("orig-lost"),
931 "original_error must carry the original error, got: {original}"
932 );
933 assert!(
934 catch.contains("catch-fail"),
935 "catch_error must carry the catch error, got: {catch}"
936 );
937 }
938
939 #[test]
940 fn catch_throws_marks_span_error_and_event() {
941 let mut boxed = catch_fails_service();
942 let (result, captured, span_records) = capture_debugs_with_span_records(|| {
943 let span = tracing::info_span!("dotry_test", error = tracing::field::Empty);
945 let _guard = span.enter();
946 tokio::runtime::Builder::new_current_thread()
947 .enable_all()
948 .build()
949 .expect("current-thread runtime")
950 .block_on(async { boxed.ready().await.unwrap().call(Exchange::default()).await })
951 });
952
953 assert!(
954 matches!(result, Err(CamelError::Io(_))),
955 "catch failure must return Err(Io), got: {result:?}"
956 );
957 assert!(
961 span_records.iter().any(|line| {
962 captured_field(line, "error").is_some_and(|v| v.contains("catch-fail"))
963 }),
964 "expected span error record carrying the catch error, span records: {span_records:?}"
965 );
966 let original = record_field(
968 &captured,
969 "do_try catch block failed",
970 "original_error",
971 )
972 .unwrap_or_else(|| {
973 panic!(
974 "envelope record missing original_error field under active span, captured: {captured:?}"
975 )
976 });
977 assert!(
978 original.contains("orig-lost"),
979 "original_error must carry the original error under an active span, got: {original}"
980 );
981 }
982
983 #[test]
984 fn catch_and_finally_throw_logs_finally_error() {
985 let try_step = always_fail(CamelError::ProcessorError("orig".into()));
986 let catch_step = always_fail(CamelError::Io("catch-fail".into()));
987 let finally_step = always_fail(CamelError::Config("fin-fail".into()));
988 let mut svc = DoTryService::new(vec![try_step]);
989 svc.catch_clauses.push(CatchClause {
990 matcher: CatchMatcher::ByVariant(vec!["ProcessorError".into()]),
991 on_when: None,
992 steps: vec![catch_step],
993 disposition: ExceptionDisposition::Handled,
994 });
995 svc.finally_steps = vec![finally_step];
996
997 let mut boxed = BoxProcessor::new(svc);
998 let (result, captured, _span_records) = capture_debugs_with_span_records(|| {
999 tokio::runtime::Builder::new_current_thread()
1000 .enable_all()
1001 .build()
1002 .expect("current-thread runtime")
1003 .block_on(async { boxed.ready().await.unwrap().call(Exchange::default()).await })
1004 });
1005
1006 assert!(
1007 matches!(result, Err(CamelError::Io(_))),
1008 "catch error must be restored over finally error, got: {result:?}"
1009 );
1010 let catch = record_field(
1011 &captured,
1012 "doFinally threw after failed catch; restoring catch error",
1013 "catch_error",
1014 )
1015 .unwrap_or_else(|| {
1016 panic!("catch-failed restore record missing catch_error field, captured: {captured:?}")
1017 });
1018 let finally = record_field(
1019 &captured,
1020 "doFinally threw after failed catch; restoring catch error",
1021 "finally_error",
1022 )
1023 .unwrap_or_else(|| {
1024 panic!(
1025 "catch-failed restore record missing finally_error field, captured: {captured:?}"
1026 )
1027 });
1028 assert!(
1029 catch.contains("catch-fail"),
1030 "catch_error must carry the catch error, got: {catch}"
1031 );
1032 assert!(
1033 finally.contains("fin-fail"),
1034 "finally_error must carry the finally error, got: {finally}"
1035 );
1036 }
1037
1038 use camel_api::{ExpressionErrorClass, FilterPredicate, PredicateSource};
1041
1042 fn expression_failed() -> CamelError {
1043 CamelError::ExpressionFailed {
1044 language: "rhai".to_string(),
1045 route_id: "r1".to_string(),
1046 step_id: "step#0".to_string(),
1047 verb: "when".to_string(),
1048 class: ExpressionErrorClass::Runtime,
1049 position: None,
1050 conversion: None,
1051 cause: None,
1052 }
1053 }
1054
1055 fn async_err_predicate(err: CamelError) -> PredicateSource {
1056 PredicateSource::Async(Arc::new(move |_: &Exchange| {
1057 let err = err.clone();
1058 Box::pin(async move { Err(err) }) as camel_api::BoxBoolFuture
1059 }))
1060 }
1061
1062 #[tokio::test]
1063 async fn catch_when_predicate_error_chains_original() {
1064 let try_step = always_fail(CamelError::ProcessorError("boom".into()));
1065 let mut svc = DoTryService::new(vec![try_step]);
1066 svc.catch_clauses.push(CatchClause {
1067 matcher: CatchMatcher::Predicate(async_err_predicate(expression_failed())),
1068 on_when: None,
1069 steps: vec![passthrough()],
1070 disposition: ExceptionDisposition::Handled,
1071 });
1072
1073 let mut boxed = BoxProcessor::new(svc);
1074 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
1075 match result {
1076 Err(CamelError::ExpressionFailed {
1077 cause: Some(cause), ..
1078 }) => {
1079 assert!(
1080 cause.to_string().contains("boom"),
1081 "cause must carry the original error, got: {cause}"
1082 );
1083 }
1084 other => panic!("expected Err(ExpressionFailed) with chained cause, got {other:?}"),
1085 }
1086 }
1087
1088 #[tokio::test]
1089 async fn catch_on_when_predicate_error_chains_original() {
1090 let try_step = always_fail(CamelError::ProcessorError("boom".into()));
1091 let mut svc = DoTryService::new(vec![try_step]);
1092 svc.catch_clauses.push(CatchClause {
1093 matcher: CatchMatcher::ByVariant(vec!["*".into()]),
1094 on_when: Some(async_err_predicate(expression_failed())),
1095 steps: vec![passthrough()],
1096 disposition: ExceptionDisposition::Handled,
1097 });
1098
1099 let mut boxed = BoxProcessor::new(svc);
1100 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
1101 match result {
1102 Err(CamelError::ExpressionFailed {
1103 cause: Some(cause), ..
1104 }) => {
1105 assert!(
1106 cause.to_string().contains("boom"),
1107 "cause must carry the original error, got: {cause}"
1108 );
1109 }
1110 other => panic!("expected Err(ExpressionFailed) with chained cause, got {other:?}"),
1111 }
1112 }
1113
1114 #[tokio::test]
1115 async fn finally_on_when_predicate_error_fails() {
1116 let mut svc = DoTryService::new(vec![passthrough()]);
1117 svc.finally_steps = vec![passthrough()];
1118 svc.finally_on_when = Some(async_err_predicate(expression_failed()));
1119
1120 let mut boxed = BoxProcessor::new(svc);
1121 let result = boxed.ready().await.unwrap().call(Exchange::default()).await;
1122 match result {
1123 Err(CamelError::ExpressionFailed { .. }) => {}
1124 other => panic!("expected Err(ExpressionFailed), got {other:?}"),
1125 }
1126 }
1127
1128 use camel_language_api::{EvalMeta, LanguageError, to_expression_failed};
1131
1132 fn caller_diagnostic_failed(cause: CamelError) -> CamelError {
1133 CamelError::ExpressionFailed {
1134 language: "rhai".to_string(),
1135 route_id: "r1".to_string(),
1136 step_id: "s1".to_string(),
1137 verb: "when".to_string(),
1138 class: ExpressionErrorClass::Runtime,
1139 position: None,
1140 conversion: None,
1141 cause: Some(Box::new(cause)),
1142 }
1143 }
1144
1145 #[test]
1146 fn chain_replaces_mapper_generated_cause() {
1147 let meta = EvalMeta {
1148 language: "xpath".into(),
1149 route_id: "r1".into(),
1150 step_id: "s1".into(),
1151 verb: "when".into(),
1152 target: None,
1153 };
1154 let mapper_err = to_expression_failed(
1155 LanguageError::EvalFailure {
1156 class: ExpressionErrorClass::Parse,
1157 position: None,
1158 detail: Some("body is not valid XML".into()),
1159 },
1160 &meta,
1161 );
1162
1163 let result = chain_predicate_error(mapper_err, CamelError::ProcessorError("boom".into()));
1164
1165 match result {
1166 CamelError::ExpressionFailed {
1167 cause: Some(cause), ..
1168 } => {
1169 assert!(
1170 cause.to_string().contains("boom"),
1171 "mapper diagnostic cause must be replaced by the original, got: {cause}"
1172 );
1173 }
1174 other => panic!("expected ExpressionFailed with chained cause, got {other:?}"),
1175 }
1176 }
1177
1178 #[test]
1179 fn chain_preserves_caller_cause() {
1180 let predicate_err =
1181 caller_diagnostic_failed(CamelError::ProcessorError("caller diagnostic".into()));
1182
1183 let result =
1184 chain_predicate_error(predicate_err, CamelError::ProcessorError("boom".into()));
1185
1186 match result {
1187 CamelError::ExpressionFailed {
1188 cause: Some(cause), ..
1189 } => {
1190 let rendered = cause.to_string();
1191 assert!(
1192 rendered.contains("caller diagnostic"),
1193 "caller cause must pass through untouched, got: {rendered}"
1194 );
1195 assert!(
1196 !rendered.contains("boom"),
1197 "caller cause must not be replaced, got: {rendered}"
1198 );
1199 }
1200 other => panic!("expected ExpressionFailed with caller cause, got {other:?}"),
1201 }
1202 }
1203
1204 #[test]
1205 fn chain_preserves_caller_source_bearing_cause() {
1206 let predicate_err = caller_diagnostic_failed(CamelError::ProcessorErrorWithSource(
1207 "caller diagnostic".into(),
1208 Arc::new(std::io::Error::other("caller source")),
1209 ));
1210
1211 let result =
1212 chain_predicate_error(predicate_err, CamelError::ProcessorError("boom".into()));
1213
1214 match result {
1215 CamelError::ExpressionFailed {
1216 cause: Some(cause), ..
1217 } => {
1218 let rendered = cause.to_string();
1219 assert!(
1220 rendered.contains("caller diagnostic"),
1221 "caller cause must pass through untouched, got: {rendered}"
1222 );
1223 let source = std::error::Error::source(cause.as_ref())
1224 .unwrap_or_else(|| panic!("caller source must survive, cause: {cause:?}"));
1225 assert!(
1226 source.to_string().contains("caller source"),
1227 "caller source must survive untouched, got: {source}"
1228 );
1229 }
1230 other => panic!("expected ExpressionFailed with caller cause, got {other:?}"),
1231 }
1232 }
1233}