1use std::future::Future;
2use std::pin::Pin;
3use std::task::{Context, Poll};
4
5use tower::{Layer, Service, ServiceExt};
6
7use camel_api::error_handler::{
8 BoundaryKind, ExceptionDisposition, ExceptionPolicy, HEADER_REDELIVERED,
9 HEADER_REDELIVERY_COUNTER, HEADER_REDELIVERY_MAX_COUNTER, PolicyId, RetryOutcome,
10 StepDisposition,
11};
12use camel_api::{BoxProcessor, CamelError, Exchange, PipelineOutcome, SyncBoxProcessor, Value};
13
14async fn execute_on_steps(
15 original: Exchange,
16 original_err: CamelError,
17 on_steps: &SyncBoxProcessor,
18 disposition: ExceptionDisposition,
19 handler: Option<BoxProcessor>,
20) -> Result<Exchange, CamelError> {
21 let snapshot = original.clone();
22 let mut ex = original;
23 ex.set_error(original_err.clone());
24 let mut pipeline = on_steps.clone_inner();
25 let step_result = async {
26 let svc = pipeline.ready().await?;
27 svc.call(ex).await
28 }
29 .await;
30
31 match step_result {
32 Ok(mut ex) => {
33 if disposition == ExceptionDisposition::Handled {
34 ex.handle_error();
35 Ok(ex)
36 } else {
37 Err(original_err)
40 }
41 }
42 Err(_) => {
43 tracing::warn!(error = %original_err, "on_steps pipeline failed, falling back to handler/DLC");
45 let mut ex = snapshot;
46 ex.set_error(original_err);
47 send_to_handler(ex, handler).await
48 }
49 }
50}
51
52pub async fn invoke_processor(
57 svc: &mut BoxProcessor,
58 ex: Exchange,
59) -> Result<Exchange, CamelError> {
60 match svc.ready().await {
61 Ok(ready) => ready.call(ex).await,
62 Err(err) => Err(err),
63 }
64}
65
66#[async_trait::async_trait]
71pub trait RouteErrorHandler: Send + Sync {
72 fn match_policy(&self, err: &CamelError) -> Option<PolicyId>;
74
75 async fn retry_step(
77 &self,
78 policy: Option<PolicyId>,
79 step: &mut dyn camel_api::error_handler::RetryableStep,
80 original: Exchange,
81 error: CamelError,
82 ) -> RetryOutcome;
83
84 async fn handle_step(
86 &self,
87 policy: Option<PolicyId>,
88 exchange: Exchange,
89 error: CamelError,
90 ) -> Result<StepDisposition, CamelError>;
91
92 async fn handle_boundary(
94 &self,
95 kind: BoundaryKind,
96 exchange: Exchange,
97 error: CamelError,
98 ) -> Result<Exchange, CamelError>;
99}
100
101pub struct DefaultRouteErrorHandler {
107 pub(crate) dlc_producer: Option<SyncBoxProcessor>,
108 pub(crate) policies: Vec<(ExceptionPolicy, Option<SyncBoxProcessor>)>,
109 pub use_original_message: bool,
113}
114
115impl DefaultRouteErrorHandler {
116 pub fn new(
117 dlc_producer: Option<BoxProcessor>,
118 policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
119 ) -> Self {
120 Self {
121 dlc_producer: dlc_producer.map(SyncBoxProcessor::new),
122 policies: policies
123 .into_iter()
124 .map(|(p, prod)| (p, prod.map(SyncBoxProcessor::new)))
125 .collect(),
126 use_original_message: false,
127 }
128 }
129
130 pub fn with_use_original_message(mut self, enabled: bool) -> Self {
132 self.use_original_message = enabled;
133 self
134 }
135
136 fn restore_original_message_if_enabled(&self, exchange: &mut Exchange) {
140 if self.use_original_message
141 && let Some(orig) =
142 exchange.get_extension::<camel_api::Message>(camel_api::ORIGINAL_MESSAGE_EXTENSION)
143 {
144 exchange.input = orig.clone();
145 }
146 }
147
148 fn resolve_producer(
151 &self,
152 policy: Option<PolicyId>,
153 ) -> (ExceptionDisposition, Option<BoxProcessor>) {
154 match policy {
155 Some(PolicyId(idx)) => match self.policies.get(idx) {
156 Some((p, prod)) => (
157 p.disposition,
158 prod.as_ref()
159 .map(|p| p.clone_inner())
160 .or_else(|| self.dlc_producer.as_ref().map(|p| p.clone_inner())),
161 ),
162 None => (
163 ExceptionDisposition::Propagate,
164 self.dlc_producer.as_ref().map(|p| p.clone_inner()),
165 ),
166 },
167 None => (
168 ExceptionDisposition::Propagate,
169 self.dlc_producer.as_ref().map(|p| p.clone_inner()),
170 ),
171 }
172 }
173}
174
175#[async_trait::async_trait]
176impl RouteErrorHandler for DefaultRouteErrorHandler {
177 fn match_policy(&self, err: &CamelError) -> Option<PolicyId> {
178 self.policies
179 .iter()
180 .position(|(p, _)| (p.matches)(err))
181 .map(PolicyId)
182 }
183
184 async fn retry_step(
185 &self,
186 policy: Option<PolicyId>,
187 step: &mut dyn camel_api::error_handler::RetryableStep,
188 original: Exchange,
189 error: CamelError,
190 ) -> RetryOutcome {
191 let Some(PolicyId(idx)) = policy else {
192 return RetryOutcome::Exhausted {
193 exchange: original,
194 error,
195 policy: None,
196 };
197 };
198 let Some((policy_def, _)) = self.policies.get(idx) else {
199 return RetryOutcome::Exhausted {
200 exchange: original,
201 error,
202 policy,
203 };
204 };
205 let Some(ref backoff) = policy_def.retry else {
206 return RetryOutcome::Exhausted {
207 exchange: original,
208 error,
209 policy,
210 };
211 };
212
213 for attempt in 0..backoff.max_attempts {
214 let delay = backoff.delay_for(attempt);
215 tokio::time::sleep(delay).await;
216
217 let mut ex = original.clone();
218 ex.input.set_header(HEADER_REDELIVERED, Value::Bool(true));
219 ex.input.set_header(
220 HEADER_REDELIVERY_COUNTER,
221 Value::Number((attempt + 1).into()),
222 );
223 ex.input.set_header(
224 HEADER_REDELIVERY_MAX_COUNTER,
225 Value::Number(backoff.max_attempts.into()),
226 );
227
228 match step.invoke(ex).await {
229 PipelineOutcome::Completed(exchange) => {
230 return RetryOutcome::Recovered(exchange);
231 }
232 PipelineOutcome::Stopped(stopped_ex) => {
233 return RetryOutcome::Stopped(stopped_ex);
234 }
235 PipelineOutcome::Failed(retry_err) => {
236 if attempt + 1 == backoff.max_attempts {
237 let mut final_ex = original;
238 final_ex
239 .input
240 .set_header(HEADER_REDELIVERED, Value::Bool(true));
241 final_ex.input.set_header(
242 HEADER_REDELIVERY_COUNTER,
243 Value::Number(backoff.max_attempts.into()),
244 );
245 final_ex.input.set_header(
246 HEADER_REDELIVERY_MAX_COUNTER,
247 Value::Number(backoff.max_attempts.into()),
248 );
249 return RetryOutcome::Exhausted {
250 exchange: final_ex,
251 error: retry_err,
252 policy,
253 };
254 }
255 }
256 }
257 }
258
259 RetryOutcome::Exhausted {
260 exchange: original,
261 error,
262 policy,
263 }
264 }
265
266 async fn handle_step(
267 &self,
268 policy: Option<PolicyId>,
269 mut exchange: Exchange,
270 error: CamelError,
271 ) -> Result<StepDisposition, CamelError> {
272 let (disposition, producer) = self.resolve_producer(policy);
273
274 if policy.is_none() && producer.is_none() {
280 tracing::debug!(
281 kind = %error.variant_name(),
282 "no on_exceptions policy matched and no dead-letter channel configured; propagating error"
283 );
284 }
285
286 if !matches!(disposition, ExceptionDisposition::Propagate)
291 && let Some(PolicyId(idx)) = policy
292 && let Some((p, _)) = self.policies.get(idx)
293 && let Some(ref steps) = p.on_steps
294 {
295 let snapshot = exchange.clone();
296 exchange.set_error(error.clone());
297 let mut step_pipeline = steps.clone_inner();
298 let step_result = async {
299 let svc = step_pipeline.ready().await?;
300 svc.call(exchange).await
301 }
302 .await;
303 match step_result {
304 Ok(mut ex) => match disposition {
305 ExceptionDisposition::Handled => {
306 ex.handle_error();
307 return Ok(StepDisposition::Handled(ex));
308 }
309 ExceptionDisposition::Continued => {
310 ex.clear_error();
311 return Ok(StepDisposition::Continued(ex));
312 }
313 _ => {
316 exchange = snapshot;
317 }
318 },
319 Err(_) => {
320 exchange = snapshot;
321 }
322 }
323 }
324
325 self.restore_original_message_if_enabled(&mut exchange);
328 exchange.set_error(error.clone());
329 match send_to_handler(exchange, producer).await {
330 Ok(handler_ex) => match disposition {
331 ExceptionDisposition::Handled => {
332 let mut ex = handler_ex;
333 ex.clear_error();
334 Ok(StepDisposition::Handled(ex))
335 }
336 ExceptionDisposition::Continued => {
337 let mut ex = handler_ex;
338 ex.clear_error();
339 Ok(StepDisposition::Continued(ex))
340 }
341 _ => Ok(StepDisposition::Propagate(error)),
343 },
344 Err(_) => Ok(StepDisposition::Propagate(error)),
346 }
347 }
348
349 async fn handle_boundary(
350 &self,
351 boundary_kind: BoundaryKind,
352 mut exchange: Exchange,
353 error: CamelError,
354 ) -> Result<Exchange, CamelError> {
355 let policy = self.match_policy(&error);
361 let (disposition, producer) = self.resolve_producer(policy);
362
363 if policy.is_none() && producer.is_none() {
370 tracing::debug!(
371 boundary = ?boundary_kind,
372 kind = %error.variant_name(),
373 "no on_exceptions policy matched and no dead-letter channel configured; propagating error"
374 );
375 }
376
377 if !matches!(
383 disposition,
384 ExceptionDisposition::Propagate | ExceptionDisposition::Continued
385 ) && let Some(PolicyId(idx)) = policy
386 && let Some((p, _)) = self.policies.get(idx)
387 && let Some(ref steps) = p.on_steps
388 {
389 let snapshot = exchange.clone();
390 exchange.set_error(error.clone());
391 let mut step_pipeline = steps.clone_inner();
392 let step_result = async {
393 let svc = step_pipeline.ready().await?;
394 svc.call(exchange).await
395 }
396 .await;
397 match step_result {
398 Ok(mut ex) => match disposition {
399 ExceptionDisposition::Handled => {
400 ex.handle_error();
401 return Ok(ex);
402 }
403 _ => {
405 exchange = snapshot;
406 }
407 },
408 Err(_) => {
409 exchange = snapshot;
410 }
411 }
412 }
413
414 self.restore_original_message_if_enabled(&mut exchange);
416 exchange.set_error(error.clone());
417 match send_to_handler(exchange, producer).await {
418 Ok(handler_ex) => match disposition {
419 ExceptionDisposition::Handled => {
420 let mut ex = handler_ex;
421 ex.clear_error();
422 Ok(ex)
423 }
424 _ => {
426 let mut ex = handler_ex;
427 ex.set_error(error);
428 Ok(ex)
429 }
430 },
431 Err(e) => Err(e),
433 }
434 }
435}
436
437pub struct ErrorHandlerLayer {
441 dlc_producer: Option<BoxProcessor>,
443 policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
445}
446
447impl ErrorHandlerLayer {
448 pub fn new(
450 dlc_producer: Option<BoxProcessor>,
451 policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
452 ) -> Self {
453 Self {
454 dlc_producer,
455 policies,
456 }
457 }
458}
459
460impl<S> Layer<S> for ErrorHandlerLayer
461where
462 S: Service<Exchange, Response = Exchange, Error = CamelError> + Send + Clone + 'static,
463 S::Future: Send + 'static,
464{
465 type Service = ErrorHandlerService<S>;
466
467 fn layer(&self, inner: S) -> Self::Service {
468 ErrorHandlerService {
469 inner,
470 dlc_producer: self.dlc_producer.clone(),
471 policies: self
472 .policies
473 .iter()
474 .map(|(p, prod)| (p.clone(), prod.clone()))
475 .collect(),
476 }
477 }
478}
479
480pub struct ErrorHandlerService<S> {
485 inner: S,
486 dlc_producer: Option<BoxProcessor>,
487 policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
488}
489
490impl<S: Clone> Clone for ErrorHandlerService<S> {
491 fn clone(&self) -> Self {
492 Self {
493 inner: self.inner.clone(),
494 dlc_producer: self.dlc_producer.clone(),
495 policies: self
496 .policies
497 .iter()
498 .map(|(p, prod)| (p.clone(), prod.clone()))
499 .collect(),
500 }
501 }
502}
503
504impl<S> ErrorHandlerService<S>
505where
506 S: Service<Exchange, Response = Exchange, Error = CamelError> + Send + Clone + 'static,
507 S::Future: Send + 'static,
508{
509 pub fn new(
511 inner: S,
512 dlc_producer: Option<BoxProcessor>,
513 policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
514 ) -> Self {
515 Self {
516 inner,
517 dlc_producer,
518 policies,
519 }
520 }
521}
522
523impl<S> Service<Exchange> for ErrorHandlerService<S>
524where
525 S: Service<Exchange, Response = Exchange, Error = CamelError> + Send + Clone + 'static,
526 S::Future: Send + 'static,
527{
528 type Response = Exchange;
529 type Error = CamelError;
530 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
531
532 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
533 match self.inner.poll_ready(cx) {
538 Poll::Pending => Poll::Pending,
539 Poll::Ready(Err(_)) => Poll::Ready(Ok(())),
540 Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
541 }
542 }
543
544 fn call(&mut self, exchange: Exchange) -> Self::Future {
545 let mut inner = self.inner.clone();
546 let dlc = self.dlc_producer.clone();
547 let policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)> = self
548 .policies
549 .iter()
550 .map(|(p, prod)| (p.clone(), prod.clone()))
551 .collect();
552
553 Box::pin(async move {
554 let original = exchange.clone();
555 let result = match inner.ready().await {
556 Ok(svc) => svc.call(exchange).await,
557 Err(e) => Err(e), };
559
560 let err = match result {
561 Ok(ex) => return Ok(ex),
562 Err(e) => e,
563 };
564
565 let matched = policies.into_iter().find(|(p, _)| (p.matches)(&err));
567
568 if let Some((policy, policy_producer)) = matched {
569 if let Some(ref backoff) = policy.retry {
571 for attempt in 0..backoff.max_attempts {
572 let delay = backoff.delay_for(attempt);
573 tokio::time::sleep(delay).await;
574
575 let mut ex = original.clone();
577 ex.input.set_header(HEADER_REDELIVERED, Value::Bool(true));
578 ex.input.set_header(
579 HEADER_REDELIVERY_COUNTER,
580 Value::Number((attempt + 1).into()),
581 );
582 ex.input.set_header(
583 HEADER_REDELIVERY_MAX_COUNTER,
584 Value::Number(backoff.max_attempts.into()),
585 );
586
587 let result = match inner.ready().await {
588 Ok(svc) => svc.call(ex).await,
589 Err(e) => Err(e), };
591 match result {
592 Ok(ex) => return Ok(ex),
593 Err(retry_err) => {
594 if attempt + 1 == backoff.max_attempts {
595 let mut original = original.clone();
597 original
598 .input
599 .set_header(HEADER_REDELIVERED, Value::Bool(true));
600 original.input.set_header(
601 HEADER_REDELIVERY_COUNTER,
602 Value::Number(backoff.max_attempts.into()),
603 );
604 original.input.set_header(
605 HEADER_REDELIVERY_MAX_COUNTER,
606 Value::Number(backoff.max_attempts.into()),
607 );
608 if let Some(ref steps) = policy.on_steps {
609 let handler = policy_producer.clone().or(dlc.clone());
610 return execute_on_steps(
611 original,
612 retry_err,
613 steps,
614 policy.disposition,
615 handler,
616 )
617 .await;
618 }
619 original.set_error(retry_err);
620 let handler = policy_producer.or(dlc);
621 return send_to_handler(original, handler).await;
622 }
623 }
624 }
625 }
626 }
627 if let Some(ref steps) = policy.on_steps {
629 let handler = policy_producer.or(dlc);
630 return execute_on_steps(original, err, steps, policy.disposition, handler)
631 .await;
632 }
633 let mut ex = original.clone();
634 ex.set_error(err);
635 let handler = policy_producer.or(dlc);
636 send_to_handler(ex, handler).await
637 } else {
638 let mut ex = original;
640 ex.set_error(err);
641 send_to_handler(ex, dlc).await
642 }
643 })
644 }
645}
646
647async fn send_to_handler(
648 exchange: Exchange,
649 producer: Option<BoxProcessor>,
650) -> Result<Exchange, CamelError> {
651 match producer {
652 None => {
653 tracing::error!(
655 error = ?exchange.error,
656 "Exchange failed with no error handler configured"
657 );
658 Ok(exchange)
659 }
660 Some(mut prod) => match prod.ready().await {
661 Err(e) => {
662 tracing::error!("DLC/handler not ready: {e}");
664 Ok(exchange)
665 }
666 Ok(svc) => match svc.call(exchange.clone()).await {
667 Ok(ex) => Ok(ex),
668 Err(e) => {
669 tracing::error!("DLC/handler call failed: {e}");
671 Ok(exchange)
673 }
674 },
675 },
676 }
677}
678
679#[cfg(test)]
680mod tests {
681 use super::*;
682 use camel_api::{
683 BoxProcessor, BoxProcessorExt, CamelError, Exchange, Message, OutcomePipeline,
684 OutcomeSegment, PipelineOutcome, RetryableStep, SyncBoxProcessor, Value,
685 error_handler::RedeliveryPolicy,
686 };
687 use std::future::Future;
688 use std::pin::Pin;
689 use std::sync::{
690 Arc,
691 atomic::{AtomicU32, Ordering},
692 };
693 use std::time::Duration;
694 use tower::ServiceExt;
695
696 fn make_exchange() -> Exchange {
697 Exchange::new(Message::new("test"))
698 }
699
700 fn failing_processor() -> BoxProcessor {
701 BoxProcessor::from_fn(|_ex| {
702 Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
703 })
704 }
705
706 fn ok_processor() -> BoxProcessor {
707 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
708 }
709
710 fn fail_n_times(n: u32) -> BoxProcessor {
711 let count = Arc::new(AtomicU32::new(0));
712 BoxProcessor::from_fn(move |ex| {
713 let count = Arc::clone(&count);
714 Box::pin(async move {
715 let c = count.fetch_add(1, Ordering::SeqCst);
716 if c < n {
717 Err(CamelError::ProcessorError(format!("attempt {c}")))
718 } else {
719 Ok(ex)
720 }
721 })
722 })
723 }
724
725 #[tokio::test]
726 async fn test_ok_passthrough() {
727 let svc = ErrorHandlerService::new(ok_processor(), None, vec![]);
728 let result = svc.oneshot(make_exchange()).await;
729 assert!(result.is_ok());
730 assert!(!result.unwrap().has_error());
731 }
732
733 #[tokio::test]
734 async fn test_error_goes_to_dlc() {
735 let received = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
736 let received_clone = Arc::clone(&received);
737 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
738 let r = Arc::clone(&received_clone);
739 Box::pin(async move {
740 r.lock().unwrap().push(ex.clone());
741 Ok(ex)
742 })
743 });
744
745 let svc = ErrorHandlerService::new(failing_processor(), Some(dlc), vec![]);
746 let result = svc.oneshot(make_exchange()).await;
747 assert!(result.is_ok());
748 let ex = result.unwrap();
749 assert!(ex.has_error());
750 assert_eq!(received.lock().unwrap().len(), 1);
751 }
752
753 #[tokio::test]
754 async fn test_retry_recovers() {
755 let inner = fail_n_times(2);
756 let policy = ExceptionPolicy {
757 matches: Arc::new(|_| true),
758 retry: Some(RedeliveryPolicy {
759 max_attempts: 3,
760 initial_delay: Duration::from_millis(1),
761 multiplier: 1.0,
762 max_delay: Duration::from_millis(10),
763 jitter_factor: 0.0,
764 }),
765 handled_by: None,
766 on_steps: None,
767 disposition: ExceptionDisposition::Propagate,
768 };
769 let svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
770 let result = svc.oneshot(make_exchange()).await;
771 assert!(result.is_ok());
772 assert!(!result.unwrap().has_error());
773 }
774
775 #[tokio::test]
776 async fn test_retry_exhausted_goes_to_dlc() {
777 let inner = fail_n_times(10);
778 let received = Arc::new(std::sync::Mutex::new(0u32));
779 let received_clone = Arc::clone(&received);
780 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
781 let r = Arc::clone(&received_clone);
782 Box::pin(async move {
783 *r.lock().unwrap() += 1;
784 Ok(ex)
785 })
786 });
787 let policy = ExceptionPolicy {
788 matches: Arc::new(|_| true),
789 retry: Some(RedeliveryPolicy {
790 max_attempts: 2,
791 initial_delay: Duration::from_millis(1),
792 multiplier: 1.0,
793 max_delay: Duration::from_millis(10),
794 jitter_factor: 0.0,
795 }),
796 handled_by: None,
797 on_steps: None,
798 disposition: ExceptionDisposition::Propagate,
799 };
800 let svc = ErrorHandlerService::new(inner, Some(dlc), vec![(policy, None)]);
801 let result = svc.oneshot(make_exchange()).await;
802 assert!(result.is_ok());
803 assert!(result.unwrap().has_error());
804 assert_eq!(*received.lock().unwrap(), 1);
805 }
806
807 #[test]
808 fn test_poll_ready_delegates_to_inner() {
809 use std::sync::atomic::AtomicBool;
810
811 #[derive(Clone)]
813 struct DelayedReadyService {
814 ready: Arc<AtomicBool>,
815 }
816
817 impl Service<Exchange> for DelayedReadyService {
818 type Response = Exchange;
819 type Error = CamelError;
820 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
821
822 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
823 if self.ready.fetch_or(true, Ordering::SeqCst) {
824 Poll::Ready(Ok(()))
826 } else {
827 cx.waker().wake_by_ref();
829 Poll::Pending
830 }
831 }
832
833 fn call(&mut self, ex: Exchange) -> Self::Future {
834 Box::pin(async move { Ok(ex) })
835 }
836 }
837
838 let waker = futures::task::noop_waker();
839 let mut cx = Context::from_waker(&waker);
840
841 let inner = DelayedReadyService {
842 ready: Arc::new(AtomicBool::new(false)),
843 };
844 let mut svc = ErrorHandlerService::new(inner, None, vec![]);
845
846 let first = Pin::new(&mut svc).poll_ready(&mut cx);
848 assert!(first.is_pending(), "expected Pending on first poll_ready");
849
850 let second = Pin::new(&mut svc).poll_ready(&mut cx);
852 assert!(second.is_ready(), "expected Ready on second poll_ready");
853 }
854
855 #[tokio::test]
856 async fn test_no_matching_policy_uses_dlc() {
857 let received = Arc::new(std::sync::Mutex::new(0u32));
858 let received_clone = Arc::clone(&received);
859 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
860 let r = Arc::clone(&received_clone);
861 Box::pin(async move {
862 *r.lock().unwrap() += 1;
863 Ok(ex)
864 })
865 });
866 let policy = ExceptionPolicy::new(|e| matches!(e, CamelError::Io(_)));
867 let svc = ErrorHandlerService::new(failing_processor(), Some(dlc), vec![(policy, None)]);
868 let result = svc.oneshot(make_exchange()).await;
869 assert!(result.is_ok());
870 assert_eq!(*received.lock().unwrap(), 1);
871 }
872
873 #[tokio::test]
874 async fn test_redelivery_headers_are_set() {
875 use camel_api::error_handler::{
876 HEADER_REDELIVERED, HEADER_REDELIVERY_COUNTER, HEADER_REDELIVERY_MAX_COUNTER,
877 RedeliveryPolicy,
878 };
879
880 let inner = fail_n_times(10);
881 let received = Arc::new(std::sync::Mutex::new(None));
882 let received_clone = Arc::clone(&received);
883 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
884 let r = Arc::clone(&received_clone);
885 Box::pin(async move {
886 *r.lock().unwrap() = Some(ex.clone());
887 Ok(ex)
888 })
889 });
890
891 let policy = ExceptionPolicy {
892 matches: Arc::new(|_| true),
893 retry: Some(RedeliveryPolicy {
894 max_attempts: 2,
895 initial_delay: Duration::from_millis(1),
896 multiplier: 1.0,
897 max_delay: Duration::from_millis(10),
898 jitter_factor: 0.0,
899 }),
900 handled_by: None,
901 on_steps: None,
902 disposition: ExceptionDisposition::Propagate,
903 };
904
905 let svc = ErrorHandlerService::new(inner, Some(dlc), vec![(policy, None)]);
906 let _ = svc.oneshot(make_exchange()).await.unwrap();
907
908 let ex = received.lock().unwrap().take().unwrap();
909 assert_eq!(
910 ex.input.header(HEADER_REDELIVERED),
911 Some(&Value::Bool(true))
912 );
913 assert_eq!(
914 ex.input.header(HEADER_REDELIVERY_COUNTER),
915 Some(&Value::Number(2.into()))
916 );
917 assert_eq!(
918 ex.input.header(HEADER_REDELIVERY_MAX_COUNTER),
919 Some(&Value::Number(2.into()))
920 );
921 }
922
923 #[tokio::test]
924 async fn test_jitter_produces_varying_delays_in_retry_flow() {
925 use std::time::Instant;
926
927 let inner = fail_n_times(10);
928 let received = Arc::new(std::sync::Mutex::new(None));
929 let received_clone = Arc::clone(&received);
930 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
931 let r = Arc::clone(&received_clone);
932 Box::pin(async move {
933 *r.lock().unwrap() = Some(ex.clone());
934 Ok(ex)
935 })
936 });
937
938 let policy = ExceptionPolicy {
939 matches: Arc::new(|_| true),
940 retry: Some(RedeliveryPolicy {
941 max_attempts: 5,
942 initial_delay: Duration::from_millis(20),
943 multiplier: 1.0,
944 max_delay: Duration::from_millis(100),
945 jitter_factor: 0.5,
946 }),
947 handled_by: None,
948 on_steps: None,
949 disposition: ExceptionDisposition::Propagate,
950 };
951
952 let start = Instant::now();
953 let svc = ErrorHandlerService::new(inner, Some(dlc), vec![(policy, None)]);
954 let _ = svc.oneshot(make_exchange()).await.unwrap();
955 let elapsed = start.elapsed();
956
957 assert!(
958 received.lock().unwrap().is_some(),
959 "DLC should have received exchange"
960 );
961
962 assert!(
963 elapsed >= Duration::from_millis(50),
964 "5 retries with 20ms base delay should take at least 50ms (with jitter low bound)"
965 );
966
967 assert!(
968 elapsed <= Duration::from_millis(500),
969 "5 retries with 20ms base delay + 50% jitter should not exceed 500ms"
970 );
971 }
972
973 #[tokio::test]
974 async fn test_on_steps_handled_true_consumes_error() {
975 use tower::ServiceExt;
976
977 let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
978 ex.input.body = camel_api::Body::Bytes("handled".into());
979 async move { Ok(ex) }
980 }));
981 let policy = ExceptionPolicy {
982 matches: Arc::new(|_| true),
983 retry: None,
984 handled_by: None,
985 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
986 disposition: ExceptionDisposition::Handled,
987 };
988 let inner = tower::service_fn(|_ex: Exchange| async {
989 Err::<Exchange, CamelError>(CamelError::RouteError("fail".to_string()))
990 });
991 let mut svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
992 let ex = Exchange::default();
993 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
994 assert!(result.error.is_none(), "handled:true should clear error");
995 assert!(matches!(result.input.body, camel_api::Body::Bytes(_)));
996 }
997
998 #[tokio::test]
999 async fn test_on_steps_handled_false_propagates_error() {
1000 use tower::ServiceExt;
1001
1002 let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
1003 ex.input.body = camel_api::Body::Bytes("handled".into());
1004 async move { Ok(ex) }
1005 }));
1006 let policy = ExceptionPolicy {
1007 matches: Arc::new(|_| true),
1008 retry: None,
1009 handled_by: None,
1010 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1011 disposition: ExceptionDisposition::Propagate,
1012 };
1013 let inner = tower::service_fn(|_ex: Exchange| async {
1014 Err::<Exchange, CamelError>(CamelError::RouteError("fail".to_string()))
1015 });
1016 let mut svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
1017 let ex = Exchange::default();
1018 let result = svc.ready().await.unwrap().call(ex).await;
1019 assert!(result.is_err(), "handled:false should propagate error");
1020 }
1021
1022 #[derive(Clone)]
1030 struct ReadinessFailService {
1031 error: CamelError,
1032 }
1033
1034 impl ReadinessFailService {
1035 fn new(error: CamelError) -> Self {
1036 Self { error }
1037 }
1038 }
1039
1040 impl Service<Exchange> for ReadinessFailService {
1041 type Response = Exchange;
1042 type Error = CamelError;
1043 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
1044
1045 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1046 Poll::Ready(Err(self.error.clone()))
1047 }
1048
1049 fn call(&mut self, ex: Exchange) -> Self::Future {
1050 Box::pin(async move { Ok(ex) })
1053 }
1054 }
1055
1056 #[tokio::test]
1057 async fn test_readiness_error_goes_to_dlc() {
1058 let readiness_err = CamelError::ProcessorError("readiness-fail".into());
1059 let inner = ReadinessFailService {
1060 error: readiness_err,
1061 };
1062
1063 let received = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
1064 let received_clone = Arc::clone(&received);
1065 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1066 let r = Arc::clone(&received_clone);
1067 Box::pin(async move {
1068 r.lock().unwrap().push(ex.clone());
1069 Ok(ex)
1070 })
1071 });
1072
1073 let svc = ErrorHandlerService::new(inner, Some(dlc), vec![]);
1074 let result = svc.oneshot(make_exchange()).await;
1075
1076 assert!(
1078 result.is_ok(),
1079 "readiness error should be captured and sent to DLC, got: {:?}",
1080 result
1081 );
1082 let ex = result.unwrap();
1083 assert!(ex.has_error(), "exchange should carry the readiness error");
1084 assert_eq!(
1085 received.lock().unwrap().len(),
1086 1,
1087 "DLC should have received the exchange exactly once"
1088 );
1089 }
1090
1091 #[tokio::test]
1092 async fn test_readiness_error_goes_to_matching_policy() {
1093 let readiness_err = CamelError::ProcessorError("readiness-fail".into());
1094 let inner = ReadinessFailService {
1095 error: readiness_err,
1096 };
1097
1098 let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
1099 ex.input.body = camel_api::Body::Bytes("handled-readiness".into());
1100 async move { Ok(ex) }
1101 }));
1102 let policy = ExceptionPolicy {
1103 matches: Arc::new(|_| true),
1104 retry: None,
1105 handled_by: None,
1106 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1107 disposition: ExceptionDisposition::Handled,
1108 };
1109
1110 let svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
1111 let result = svc.oneshot(make_exchange()).await;
1112
1113 assert!(
1115 result.is_ok(),
1116 "readiness error should be captured by policy, got: {:?}",
1117 result
1118 );
1119 let ex = result.unwrap();
1120 assert!(ex.error.is_none(), "handled:true should clear error");
1121 assert!(
1122 matches!(ex.input.body, camel_api::Body::Bytes(_)),
1123 "on_steps should have modified the body"
1124 );
1125 }
1126
1127 #[test]
1128 fn test_poll_ready_converts_readiness_error_to_ok() {
1129 let readiness_err = CamelError::ProcessorError("readiness-fail".into());
1130 let inner = ReadinessFailService {
1131 error: readiness_err,
1132 };
1133 let mut svc = ErrorHandlerService::new(inner, None, vec![]);
1134
1135 let waker = futures::task::noop_waker();
1136 let mut cx = Context::from_waker(&waker);
1137
1138 let poll = Pin::new(&mut svc).poll_ready(&mut cx);
1140 match poll {
1141 Poll::Ready(Ok(())) => { }
1142 Poll::Ready(Err(e)) => panic!("poll_ready leaked readiness error: {:?}", e),
1143 Poll::Pending => panic!("poll_ready should be Ready for readiness errors"),
1144 }
1145 }
1146
1147 #[tokio::test]
1150 async fn test_invoke_processor_returns_ok_on_success() {
1151 let mut svc = ok_processor();
1152 let ex = make_exchange();
1153 let result = invoke_processor(&mut svc, ex).await;
1154 assert!(result.is_ok());
1155 }
1156
1157 #[tokio::test]
1158 async fn test_invoke_processor_captures_readiness_error() {
1159 let mut failing_ready: BoxProcessor = BoxProcessor::new(ReadinessFailService::new(
1160 CamelError::ProcessorError("not ready".into()),
1161 ));
1162 let ex = make_exchange();
1163 let result = invoke_processor(&mut failing_ready, ex).await;
1164 assert!(result.is_err());
1165 }
1166
1167 #[tokio::test]
1168 async fn test_on_steps_handled_true_clears_exception_properties() {
1169 use tower::ServiceExt;
1170
1171 let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
1172 ex.input.body = camel_api::Body::Bytes("handled".into());
1173 async move { Ok(ex) }
1174 }));
1175 let policy = ExceptionPolicy {
1176 matches: Arc::new(|_| true),
1177 retry: None,
1178 handled_by: None,
1179 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1180 disposition: ExceptionDisposition::Handled,
1181 };
1182 let inner = tower::service_fn(|_ex: Exchange| async {
1183 Err::<Exchange, CamelError>(CamelError::RouteError("fail".to_string()))
1184 });
1185 let mut svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
1186 let ex = Exchange::default();
1187 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1188 assert!(result.error.is_none(), "handled:true should clear error");
1189 assert!(
1190 !result
1191 .properties
1192 .contains_key(camel_api::exchange::PROPERTY_EXCEPTION_MESSAGE),
1193 "handled:true should clear exception properties"
1194 );
1195 assert!(
1196 !result
1197 .properties
1198 .contains_key(camel_api::exchange::PROPERTY_EXCEPTION_KIND),
1199 "handled:true should clear exception kind property"
1200 );
1201 assert!(
1202 !result
1203 .properties
1204 .contains_key(camel_api::exchange::PROPERTY_EXCEPTION_CAUGHT),
1205 "handled:true should clear exception caught property"
1206 );
1207 }
1208
1209 #[test]
1212 fn test_match_policy_returns_id_for_matching_error() {
1213 let handler = DefaultRouteErrorHandler::new(
1214 None,
1215 vec![(
1216 ExceptionPolicy::new(|e| matches!(e, CamelError::ProcessorError(_))),
1217 None,
1218 )],
1219 );
1220 let id = handler.match_policy(&CamelError::ProcessorError("test".into()));
1221 assert_eq!(id, Some(PolicyId(0)));
1222 }
1223
1224 #[test]
1225 fn test_match_policy_returns_none_for_unmatched() {
1226 let handler = DefaultRouteErrorHandler::new(None, vec![]);
1227 let id = handler.match_policy(&CamelError::ProcessorError("test".into()));
1228 assert_eq!(id, None);
1229 }
1230
1231 #[tokio::test]
1234 async fn test_retry_step_succeeds_on_second_attempt() {
1235 let mut policy = ExceptionPolicy::new(|_| true);
1236 policy.retry = Some(RedeliveryPolicy::new(3));
1237 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1238 let mut step = fail_n_times(1); let ex = make_exchange();
1240 let outcome = handler
1241 .retry_step(
1242 Some(PolicyId(0)),
1243 &mut step,
1244 ex,
1245 CamelError::ProcessorError("attempt 0".into()),
1246 )
1247 .await;
1248 assert!(matches!(outcome, RetryOutcome::Recovered(_)));
1249 }
1250
1251 #[tokio::test]
1252 async fn test_retry_step_exhausted_when_all_fail() {
1253 let mut policy = ExceptionPolicy::new(|_| true);
1254 policy.retry = Some(RedeliveryPolicy::new(3));
1255 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1256 let mut step = failing_processor();
1257 let ex = make_exchange();
1258 let outcome = handler
1259 .retry_step(
1260 Some(PolicyId(0)),
1261 &mut step,
1262 ex,
1263 CamelError::ProcessorError("boom".into()),
1264 )
1265 .await;
1266 assert!(matches!(outcome, RetryOutcome::Exhausted { .. }));
1267 }
1268
1269 #[tokio::test]
1270 async fn test_retry_step_no_policy_returns_exhausted_immediately() {
1271 let handler = DefaultRouteErrorHandler::new(None, vec![]);
1272 let mut step = ok_processor();
1273 let ex = make_exchange();
1274 let outcome = handler
1275 .retry_step(
1276 None,
1277 &mut step,
1278 ex,
1279 CamelError::ProcessorError("boom".into()),
1280 )
1281 .await;
1282 assert!(matches!(
1283 outcome,
1284 RetryOutcome::Exhausted { policy: None, .. }
1285 ));
1286 }
1287
1288 #[tokio::test]
1291 async fn test_handle_step_propagate_sends_to_dlc() {
1292 let dlc = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
1293 let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1294 let ex = make_exchange();
1295 let result = handler
1296 .handle_step(None, ex, CamelError::ProcessorError("boom".into()))
1297 .await;
1298 assert!(matches!(result, Ok(StepDisposition::Propagate(_))));
1299 }
1300
1301 #[tokio::test]
1302 async fn test_handle_step_handled_uses_handler_output() {
1303 let handler_producer = BoxProcessor::from_fn(|mut ex| {
1304 Box::pin(async move {
1305 ex.input.set_header("processed_by", Value::Bool(true));
1306 Ok(ex)
1307 })
1308 });
1309 let policy = ExceptionPolicy {
1310 matches: std::sync::Arc::new(|_| true),
1311 retry: None,
1312 handled_by: None,
1313 on_steps: None,
1314 disposition: ExceptionDisposition::Handled,
1315 };
1316 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, Some(handler_producer))]);
1317 let mut ex = make_exchange();
1318 ex.set_error(CamelError::ProcessorError("boom".into()));
1319 let result = handler
1320 .handle_step(
1321 Some(PolicyId(0)),
1322 ex,
1323 CamelError::ProcessorError("boom".into()),
1324 )
1325 .await;
1326 match result {
1327 Ok(StepDisposition::Handled(ex)) => {
1328 assert!(!ex.has_error(), "error should be cleared");
1329 assert_eq!(
1330 ex.input.header("processed_by"),
1331 Some(&Value::Bool(true)),
1332 "should use handler's output exchange"
1333 );
1334 }
1335 other => panic!("expected Handled, got {:?}", other.is_ok()),
1336 }
1337 }
1338
1339 #[tokio::test]
1340 async fn test_handle_step_continued_clears_error() {
1341 let policy = ExceptionPolicy {
1342 matches: std::sync::Arc::new(|_| true),
1343 retry: None,
1344 handled_by: None,
1345 on_steps: None,
1346 disposition: ExceptionDisposition::Continued,
1347 };
1348 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1349 let mut ex = make_exchange();
1350 ex.set_error(CamelError::ProcessorError("boom".into()));
1351 let result = handler
1352 .handle_step(
1353 Some(PolicyId(0)),
1354 ex,
1355 CamelError::ProcessorError("boom".into()),
1356 )
1357 .await;
1358 match result {
1359 Ok(StepDisposition::Continued(ex)) => assert!(!ex.has_error()),
1360 other => panic!("expected Continued, got {:?}", other.is_ok()),
1361 }
1362 }
1363
1364 #[tokio::test]
1365 async fn test_handle_step_with_on_steps_handled() {
1366 let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
1367 ex.input.body = camel_api::Body::Bytes("on_steps_ran".into());
1368 async move { Ok(ex) }
1369 }));
1370 let policy = ExceptionPolicy {
1371 matches: std::sync::Arc::new(|_| true),
1372 retry: None,
1373 handled_by: None,
1374 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1375 disposition: ExceptionDisposition::Handled,
1376 };
1377 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1378 let mut ex = make_exchange();
1379 ex.set_error(CamelError::ProcessorError("boom".into()));
1380 let result = handler
1381 .handle_step(
1382 Some(PolicyId(0)),
1383 ex,
1384 CamelError::ProcessorError("boom".into()),
1385 )
1386 .await;
1387 match result {
1388 Ok(StepDisposition::Handled(ex)) => {
1389 assert!(!ex.has_error(), "error should be cleared");
1390 assert!(
1391 matches!(ex.input.body, camel_api::Body::Bytes(_)),
1392 "on_steps should have modified the body"
1393 );
1394 }
1395 other => panic!("expected Handled, got: {}", other.is_ok()),
1396 }
1397 }
1398
1399 #[tokio::test]
1400 async fn test_handle_step_dlc_failure_propagates() {
1401 let failing_dlc = BoxProcessor::from_fn(|_| {
1402 Box::pin(async { Err(CamelError::ProcessorError("dlc broken".into())) })
1403 });
1404 let handler = DefaultRouteErrorHandler::new(Some(failing_dlc), vec![]);
1405 let ex = make_exchange();
1406 let result = handler
1407 .handle_step(None, ex, CamelError::ProcessorError("original".into()))
1408 .await;
1409 assert!(
1410 matches!(result, Ok(StepDisposition::Propagate(_))),
1411 "DLC failure should still return Propagate with original error"
1412 );
1413 }
1414
1415 #[tokio::test]
1418 async fn test_handle_boundary_security_error_goes_to_dlc() {
1419 let dlc_count = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
1420 let count_clone = dlc_count.clone();
1421 let dlc = BoxProcessor::from_fn(move |ex| {
1422 let c = count_clone.clone();
1423 Box::pin(async move {
1424 c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1425 Ok(ex)
1426 })
1427 });
1428 let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1429 let ex = make_exchange();
1430 let result = handler
1431 .handle_boundary(
1432 BoundaryKind::Security,
1433 ex,
1434 CamelError::Unauthorized("denied".into()),
1435 )
1436 .await;
1437 assert!(result.is_ok());
1438 assert_eq!(dlc_count.load(std::sync::atomic::Ordering::SeqCst), 1);
1439 }
1440
1441 #[tokio::test]
1442 async fn test_handle_boundary_handled_clears_error() {
1443 let policy = ExceptionPolicy {
1444 matches: std::sync::Arc::new(|_| true),
1445 retry: None,
1446 handled_by: None,
1447 on_steps: None,
1448 disposition: ExceptionDisposition::Handled,
1449 };
1450 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1451 let ex = make_exchange();
1452 let result = handler
1453 .handle_boundary(
1454 BoundaryKind::Security,
1455 ex,
1456 CamelError::Unauthorized("denied".into()),
1457 )
1458 .await;
1459 assert!(result.is_ok());
1460 assert!(
1461 !result.unwrap().has_error(),
1462 "Handled disposition should clear error"
1463 );
1464 }
1465
1466 #[tokio::test]
1467 async fn test_handle_boundary_propagate_preserves_error() {
1468 let policy = ExceptionPolicy {
1469 matches: std::sync::Arc::new(|_| true),
1470 retry: None,
1471 handled_by: None,
1472 on_steps: None,
1473 disposition: ExceptionDisposition::Propagate,
1474 };
1475 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1476 let ex = make_exchange();
1477 let result = handler
1478 .handle_boundary(
1479 BoundaryKind::CircuitBreaker,
1480 ex,
1481 CamelError::CircuitOpen("open".into()),
1482 )
1483 .await;
1484 assert!(result.is_ok(), "boundary errors always return Ok");
1485 assert!(
1486 result.unwrap().has_error(),
1487 "Propagate disposition should preserve error"
1488 );
1489 }
1490
1491 #[tokio::test]
1492 async fn test_handle_boundary_continued_preserves_error_like_propagate() {
1493 let dlc_count = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
1494 let count_clone = dlc_count.clone();
1495 let dlc = BoxProcessor::from_fn(move |ex| {
1496 let c = count_clone.clone();
1497 Box::pin(async move {
1498 c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1499 Ok(ex)
1500 })
1501 });
1502 let policy = ExceptionPolicy {
1503 matches: std::sync::Arc::new(|_| true),
1504 retry: None,
1505 handled_by: None,
1506 on_steps: None,
1507 disposition: ExceptionDisposition::Continued,
1508 };
1509 let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![(policy, None)]);
1510 let ex = make_exchange();
1511 let result = handler
1512 .handle_boundary(
1513 BoundaryKind::Security,
1514 ex,
1515 CamelError::Unauthorized("denied".into()),
1516 )
1517 .await;
1518 assert!(result.is_ok(), "boundary errors always return Ok");
1519 assert!(
1520 result.unwrap().has_error(),
1521 "Continued at boundary should preserve error"
1522 );
1523 assert_eq!(
1524 dlc_count.load(std::sync::atomic::Ordering::SeqCst),
1525 1,
1526 "DLC should be called"
1527 );
1528 }
1529
1530 #[tokio::test]
1531 async fn test_handle_boundary_with_on_steps_handled() {
1532 let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
1533 ex.input.body = camel_api::Body::Bytes("on_steps_ran".into());
1534 async move { Ok(ex) }
1535 }));
1536 let policy = ExceptionPolicy {
1537 matches: std::sync::Arc::new(|_| true),
1538 retry: None,
1539 handled_by: None,
1540 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1541 disposition: ExceptionDisposition::Handled,
1542 };
1543 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1544 let ex = make_exchange();
1545 let result = handler
1546 .handle_boundary(
1547 BoundaryKind::Security,
1548 ex,
1549 CamelError::Unauthorized("denied".into()),
1550 )
1551 .await;
1552 assert!(result.is_ok(), "boundary errors always return Ok");
1553 let ex = result.unwrap();
1554 assert!(!ex.has_error(), "Handled disposition should clear error");
1555 assert!(
1556 matches!(ex.input.body, camel_api::Body::Bytes(_)),
1557 "on_steps should have modified the body"
1558 );
1559 }
1560
1561 #[tokio::test]
1562 async fn retry_step_segment_stop_maps_to_retry_outcome_stopped() {
1563 use std::sync::Arc;
1564 use std::sync::atomic::{AtomicUsize, Ordering};
1565
1566 #[derive(Clone)]
1567 struct StoppingSegment {
1568 n: Arc<AtomicUsize>,
1569 }
1570 impl OutcomePipeline for StoppingSegment {
1571 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
1572 Box::new(self.clone())
1573 }
1574 fn run<'a>(
1575 &'a mut self,
1576 ex: Exchange,
1577 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1578 let n = self.n.clone();
1579 Box::pin(async move {
1580 n.fetch_add(1, Ordering::SeqCst);
1581 PipelineOutcome::Stopped(ex)
1582 })
1583 }
1584 }
1585
1586 let call_count = Arc::new(AtomicUsize::new(0));
1587 let seg = OutcomeSegment::new(Box::new(StoppingSegment {
1588 n: call_count.clone(),
1589 }));
1590 let mut retryable: Box<dyn RetryableStep> = Box::new(seg);
1591
1592 let mut policy = ExceptionPolicy::new(|_e: &CamelError| true);
1593 policy.retry = Some(RedeliveryPolicy::new(3));
1594 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1595
1596 let original = Exchange::new(Message::new("retry-me"));
1597 let err = CamelError::ProcessorError("trigger retry".into());
1598 let outcome = handler
1599 .retry_step(Some(PolicyId(0)), retryable.as_mut(), original, err)
1600 .await;
1601
1602 assert!(
1603 matches!(outcome, RetryOutcome::Stopped(_)),
1604 "Segment Stop must map to RetryOutcome::Stopped, got {:?}",
1605 outcome
1606 );
1607 assert_eq!(
1608 call_count.load(Ordering::SeqCst),
1609 1,
1610 "Stop must short-circuit retry — only 1 invoke expected, got {}",
1611 call_count.load(Ordering::SeqCst)
1612 );
1613 }
1614
1615 #[tokio::test]
1616 async fn retry_step_new_signature_works_with_dlc_producer() {
1617 use std::sync::Arc;
1618 use std::sync::atomic::{AtomicUsize, Ordering};
1619
1620 #[derive(Clone)]
1621 struct CountingProducer {
1622 count: Arc<AtomicUsize>,
1623 succeed_on: usize,
1624 }
1625 impl tower::Service<Exchange> for CountingProducer {
1626 type Response = Exchange;
1627 type Error = CamelError;
1628 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
1629 fn poll_ready(
1630 &mut self,
1631 _cx: &mut std::task::Context<'_>,
1632 ) -> std::task::Poll<Result<(), Self::Error>> {
1633 std::task::Poll::Ready(Ok(()))
1634 }
1635 fn call(&mut self, ex: Exchange) -> Self::Future {
1636 let n = self.count.fetch_add(1, Ordering::SeqCst);
1637 let succeed_on = self.succeed_on;
1638 Box::pin(async move {
1639 if n >= succeed_on {
1640 Ok(ex)
1641 } else {
1642 Err(CamelError::ProcessorError("retry".into()))
1643 }
1644 })
1645 }
1646 }
1647
1648 let count = Arc::new(AtomicUsize::new(0));
1649 let producer = CountingProducer {
1650 count: count.clone(),
1651 succeed_on: 2,
1652 };
1653 let sync_bp = SyncBoxProcessor::new(BoxProcessor::new(producer));
1654 let bp1 = sync_bp.clone_inner();
1655 let bp2 = sync_bp.clone_inner();
1656 let mut retryable1: Box<dyn RetryableStep> = Box::new(bp1);
1657 let mut retryable2: Box<dyn RetryableStep> = Box::new(bp2);
1658
1659 let ex = Exchange::new(Message::new("dlc"));
1660 let outcome1 = retryable1.invoke(ex.clone()).await;
1661 let outcome2 = retryable2.invoke(ex).await;
1662 assert!(matches!(outcome1, PipelineOutcome::Failed(_)));
1663 assert!(matches!(outcome2, PipelineOutcome::Failed(_)));
1664 assert_eq!(
1665 count.load(Ordering::SeqCst),
1666 2,
1667 "DLC producer must be invoked exactly twice through SyncBoxProcessor"
1668 );
1669 drop(retryable1);
1670 drop(retryable2);
1671 drop(sync_bp);
1672 }
1673
1674 #[tokio::test]
1677 async fn test_use_original_message_restores_body_before_dlc() {
1678 let dlc_received = Arc::new(std::sync::Mutex::new(None::<Exchange>));
1681 let dlc_received_clone = Arc::clone(&dlc_received);
1682 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1683 let r = Arc::clone(&dlc_received_clone);
1684 Box::pin(async move {
1685 *r.lock().unwrap() = Some(ex.clone());
1686 Ok(ex)
1687 })
1688 });
1689
1690 let mut handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1691 handler.use_original_message = true;
1692
1693 let mut ex = make_exchange();
1695 ex.input = Message::new("original-body");
1696
1697 let original: Arc<Message> = Arc::new(ex.input.clone());
1699 ex.set_extension(camel_api::ORIGINAL_MESSAGE_EXTENSION, original);
1700
1701 ex.input.body = camel_api::Body::Bytes("mutated-body".into());
1703
1704 let result = handler
1706 .handle_step(None, ex, CamelError::ProcessorError("boom".into()))
1707 .await;
1708 assert!(matches!(result, Ok(StepDisposition::Propagate(_))));
1709
1710 let received = dlc_received
1712 .lock()
1713 .unwrap()
1714 .take()
1715 .expect("DLC should have been called");
1716 let received_text = match &received.input.body {
1717 camel_api::Body::Text(s) => s.clone(),
1718 camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
1719 camel_api::Body::Json(v) => v.to_string(),
1720 _ => String::new(),
1721 };
1722 assert_eq!(
1723 received_text, "original-body",
1724 "DLC should receive original message body, not mutated version"
1725 );
1726 }
1727
1728 #[tokio::test]
1729 async fn test_use_original_message_handle_boundary_restores_before_dlc() {
1730 let dlc_received = Arc::new(std::sync::Mutex::new(None::<Exchange>));
1731 let dlc_received_clone = Arc::clone(&dlc_received);
1732 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1733 let r = Arc::clone(&dlc_received_clone);
1734 Box::pin(async move {
1735 *r.lock().unwrap() = Some(ex.clone());
1736 Ok(ex)
1737 })
1738 });
1739
1740 let mut handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1741 handler.use_original_message = true;
1742
1743 let mut ex = make_exchange();
1744 ex.input = Message::new("orig-boundary");
1745
1746 let original: Arc<Message> = Arc::new(ex.input.clone());
1747 ex.set_extension(camel_api::ORIGINAL_MESSAGE_EXTENSION, original);
1748
1749 ex.input.body = camel_api::Body::Bytes("mutated-boundary".into());
1751
1752 let result = handler
1753 .handle_boundary(
1754 BoundaryKind::Security,
1755 ex,
1756 CamelError::Unauthorized("denied".into()),
1757 )
1758 .await;
1759 assert!(result.is_ok());
1760
1761 let received = dlc_received
1762 .lock()
1763 .unwrap()
1764 .take()
1765 .expect("DLC should have been called");
1766 let received_text = match &received.input.body {
1767 camel_api::Body::Text(s) => s.clone(),
1768 camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
1769 camel_api::Body::Json(v) => v.to_string(),
1770 _ => String::new(),
1771 };
1772 assert_eq!(
1773 received_text, "orig-boundary",
1774 "handle_boundary should restore original message before sending to DLC"
1775 );
1776 }
1777
1778 #[tokio::test]
1779 async fn test_use_original_message_false_does_not_restore() {
1780 let dlc_received = Arc::new(std::sync::Mutex::new(None::<Exchange>));
1782 let dlc_received_clone = Arc::clone(&dlc_received);
1783 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1784 let r = Arc::clone(&dlc_received_clone);
1785 Box::pin(async move {
1786 *r.lock().unwrap() = Some(ex.clone());
1787 Ok(ex)
1788 })
1789 });
1790
1791 let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1792 let mut ex = make_exchange();
1795 ex.input = Message::new("original-body");
1796
1797 let original: Arc<Message> = Arc::new(ex.input.clone());
1799 ex.set_extension(camel_api::ORIGINAL_MESSAGE_EXTENSION, original);
1800
1801 ex.input.body = camel_api::Body::Bytes("mutated-body".into());
1803
1804 let result = handler
1805 .handle_step(None, ex, CamelError::ProcessorError("boom".into()))
1806 .await;
1807 assert!(matches!(result, Ok(StepDisposition::Propagate(_))));
1808
1809 let received = dlc_received
1810 .lock()
1811 .unwrap()
1812 .take()
1813 .expect("DLC should have been called");
1814 let received_text = match &received.input.body {
1815 camel_api::Body::Text(s) => s.clone(),
1816 camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
1817 camel_api::Body::Json(v) => v.to_string(),
1818 _ => String::new(),
1819 };
1820 assert_eq!(
1821 received_text, "mutated-body",
1822 "When use_original_message=false, DLC should see the mutated body"
1823 );
1824 }
1825
1826 #[tokio::test]
1829 async fn propagate_skips_on_steps_in_handle_step() {
1830 let on_steps_called = Arc::new(AtomicU32::new(0));
1831 let on_steps_called_clone = on_steps_called.clone();
1832 let steps_pipeline = BoxProcessor::new(tower::service_fn(move |mut ex: Exchange| {
1833 let c = on_steps_called_clone.clone();
1834 Box::pin(async move {
1835 c.fetch_add(1, Ordering::SeqCst);
1836 ex.input.body = camel_api::Body::Bytes("on_steps_ran".into());
1837 Ok(ex)
1838 })
1839 }));
1840 let dlc_called = Arc::new(AtomicU32::new(0));
1841 let dlc_called_clone = dlc_called.clone();
1842 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1843 let c = dlc_called_clone.clone();
1844 Box::pin(async move {
1845 c.fetch_add(1, Ordering::SeqCst);
1846 Ok(ex)
1847 })
1848 });
1849 let policy = ExceptionPolicy {
1850 matches: std::sync::Arc::new(|_| true),
1851 retry: None,
1852 handled_by: None,
1853 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1854 disposition: ExceptionDisposition::Propagate,
1855 };
1856 let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![(policy, None)]);
1857 let mut ex = make_exchange();
1858 ex.set_error(CamelError::ProcessorError("boom".into()));
1859 let result = handler
1860 .handle_step(
1861 Some(PolicyId(0)),
1862 ex,
1863 CamelError::ProcessorError("boom".into()),
1864 )
1865 .await;
1866 assert!(
1867 matches!(result, Ok(StepDisposition::Propagate(_))),
1868 "Propagate disposition should return Propagate"
1869 );
1870 assert_eq!(
1871 on_steps_called.load(Ordering::SeqCst),
1872 0,
1873 "on_steps must NOT be called when disposition is Propagate (double side-effect bug)"
1874 );
1875 assert_eq!(
1876 dlc_called.load(Ordering::SeqCst),
1877 1,
1878 "DLC should still be called when disposition is Propagate"
1879 );
1880 }
1881
1882 #[tokio::test]
1883 async fn propagate_skips_on_steps_in_handle_boundary() {
1884 let on_steps_called = Arc::new(AtomicU32::new(0));
1885 let on_steps_called_clone = on_steps_called.clone();
1886 let steps_pipeline = BoxProcessor::new(tower::service_fn(move |mut ex: Exchange| {
1887 let c = on_steps_called_clone.clone();
1888 Box::pin(async move {
1889 c.fetch_add(1, Ordering::SeqCst);
1890 ex.input.body = camel_api::Body::Bytes("on_steps_ran".into());
1891 Ok(ex)
1892 })
1893 }));
1894 let dlc_called = Arc::new(AtomicU32::new(0));
1895 let dlc_called_clone = dlc_called.clone();
1896 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1897 let c = dlc_called_clone.clone();
1898 Box::pin(async move {
1899 c.fetch_add(1, Ordering::SeqCst);
1900 Ok(ex)
1901 })
1902 });
1903 let policy = ExceptionPolicy {
1904 matches: std::sync::Arc::new(|_| true),
1905 retry: None,
1906 handled_by: None,
1907 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1908 disposition: ExceptionDisposition::Propagate,
1909 };
1910 let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![(policy, None)]);
1911 let ex = make_exchange();
1912 let result = handler
1913 .handle_boundary(
1914 BoundaryKind::CircuitBreaker,
1915 ex,
1916 CamelError::CircuitOpen("open".into()),
1917 )
1918 .await;
1919 assert!(result.is_ok(), "boundary errors always return Ok");
1920 assert!(
1921 result.unwrap().has_error(),
1922 "Propagate disposition should preserve error"
1923 );
1924 assert_eq!(
1925 on_steps_called.load(Ordering::SeqCst),
1926 0,
1927 "on_steps must NOT be called when disposition is Propagate (double side-effect bug)"
1928 );
1929 assert_eq!(
1930 dlc_called.load(Ordering::SeqCst),
1931 1,
1932 "DLC should still be called when disposition is Propagate"
1933 );
1934 }
1935
1936 #[test]
1937 fn test_handle_step_no_match_emits_silent_propagate_diagnostic() {
1938 let handler = DefaultRouteErrorHandler::new(None, vec![]);
1941 let (result, captured) = capture_debugs(|| {
1942 tokio::runtime::Builder::new_current_thread()
1943 .enable_all()
1944 .build()
1945 .expect("current-thread runtime")
1946 .block_on(handler.handle_step(
1947 None,
1948 make_exchange(),
1949 CamelError::Unauthorized("denied".into()),
1950 ))
1951 });
1952 assert!(result.is_ok(), "step handler always returns Ok");
1953 assert!(
1954 captured.iter().any(|line| {
1955 line.contains(
1956 "no on_exceptions policy matched and no dead-letter channel configured; \
1957 propagating error",
1958 )
1959 }),
1960 "expected silent-propagate diagnostic, captured: {captured:?}"
1961 );
1962 assert!(
1963 captured
1964 .iter()
1965 .any(|line| line.contains("kind=Unauthorized")),
1966 "diagnostic should name the error kind, captured: {captured:?}"
1967 );
1968 }
1969
1970 #[test]
1971 fn test_handle_boundary_no_match_emits_silent_propagate_diagnostic() {
1972 let handler = DefaultRouteErrorHandler::new(None, vec![]);
1973 let (result, captured) = capture_debugs(|| {
1974 tokio::runtime::Builder::new_current_thread()
1975 .enable_all()
1976 .build()
1977 .expect("current-thread runtime")
1978 .block_on(handler.handle_boundary(
1979 BoundaryKind::Security,
1980 make_exchange(),
1981 CamelError::Unauthorized("denied".into()),
1982 ))
1983 });
1984 assert!(result.is_ok(), "boundary handler always returns Ok");
1985 assert!(
1986 captured.iter().any(|line| {
1987 line.contains(
1988 "no on_exceptions policy matched and no dead-letter channel configured; \
1989 propagating error",
1990 )
1991 }),
1992 "expected silent-propagate diagnostic, captured: {captured:?}"
1993 );
1994 assert!(
1995 captured.iter().any(
1996 |line| line.contains("boundary=Security") && line.contains("kind=Unauthorized")
1997 ),
1998 "diagnostic should name the boundary gate and error kind, captured: {captured:?}"
1999 );
2000 }
2001
2002 mod log_capture {
2007 use std::fmt;
2008 use std::sync::{Arc, Mutex};
2009 use tracing::field::{Field, Visit};
2010 use tracing::span::{Attributes, Record};
2011 use tracing::{Event, Id, Level, Metadata, Subscriber};
2012
2013 type Sink = Arc<Mutex<Vec<String>>>;
2014
2015 struct Recorder {
2016 events: Sink,
2017 next_span_id: std::sync::atomic::AtomicU64,
2018 }
2019
2020 struct FieldVisitor(String);
2021
2022 impl Visit for FieldVisitor {
2023 fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
2024 if !self.0.is_empty() {
2025 self.0.push(' ');
2026 }
2027 let _ = fmt::write(&mut self.0, format_args!("{}={:?}", field.name(), value));
2028 }
2029 }
2030
2031 impl Subscriber for Recorder {
2032 fn enabled(&self, _metadata: &Metadata<'_>) -> bool {
2033 true
2034 }
2035
2036 fn new_span(&self, _attrs: &Attributes<'_>) -> Id {
2037 let id = self
2038 .next_span_id
2039 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
2040 + 1;
2041 Id::from_u64(id)
2042 }
2043
2044 fn record(&self, _span: &Id, _values: &Record<'_>) {}
2045 fn record_follows_from(&self, _span: &Id, _follows_from: &Id) {}
2046
2047 fn event(&self, event: &Event<'_>) {
2048 if *event.metadata().level() >= Level::DEBUG {
2049 let mut visitor = FieldVisitor(String::new());
2050 event.record(&mut visitor);
2051 if let Ok(mut slot) = self.events.lock() {
2052 slot.push(visitor.0);
2053 }
2054 }
2055 }
2056
2057 fn enter(&self, _span: &Id) {}
2058 fn exit(&self, _span: &Id) {}
2059 }
2060
2061 pub(super) fn capture_debugs<T>(f: impl FnOnce() -> T) -> (T, Vec<String>) {
2066 let sink: Sink = Default::default();
2067 let recorder = Recorder {
2068 events: Arc::clone(&sink),
2069 next_span_id: Default::default(),
2070 };
2071 let out = tracing::subscriber::with_default(recorder, f);
2072 let collected = sink
2073 .lock()
2074 .ok()
2075 .map(|slot| slot.clone())
2076 .unwrap_or_default();
2077 (out, collected)
2078 }
2079 }
2080
2081 use log_capture::capture_debugs;
2082}