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 _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 !matches!(
369 disposition,
370 ExceptionDisposition::Propagate | ExceptionDisposition::Continued
371 ) && let Some(PolicyId(idx)) = policy
372 && let Some((p, _)) = self.policies.get(idx)
373 && let Some(ref steps) = p.on_steps
374 {
375 let snapshot = exchange.clone();
376 exchange.set_error(error.clone());
377 let mut step_pipeline = steps.clone_inner();
378 let step_result = async {
379 let svc = step_pipeline.ready().await?;
380 svc.call(exchange).await
381 }
382 .await;
383 match step_result {
384 Ok(mut ex) => match disposition {
385 ExceptionDisposition::Handled => {
386 ex.handle_error();
387 return Ok(ex);
388 }
389 _ => {
391 exchange = snapshot;
392 }
393 },
394 Err(_) => {
395 exchange = snapshot;
396 }
397 }
398 }
399
400 self.restore_original_message_if_enabled(&mut exchange);
402 exchange.set_error(error.clone());
403 match send_to_handler(exchange, producer).await {
404 Ok(handler_ex) => match disposition {
405 ExceptionDisposition::Handled => {
406 let mut ex = handler_ex;
407 ex.clear_error();
408 Ok(ex)
409 }
410 _ => {
412 let mut ex = handler_ex;
413 ex.set_error(error);
414 Ok(ex)
415 }
416 },
417 Err(e) => Err(e),
419 }
420 }
421}
422
423pub struct ErrorHandlerLayer {
427 dlc_producer: Option<BoxProcessor>,
429 policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
431}
432
433impl ErrorHandlerLayer {
434 pub fn new(
436 dlc_producer: Option<BoxProcessor>,
437 policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
438 ) -> Self {
439 Self {
440 dlc_producer,
441 policies,
442 }
443 }
444}
445
446impl<S> Layer<S> for ErrorHandlerLayer
447where
448 S: Service<Exchange, Response = Exchange, Error = CamelError> + Send + Clone + 'static,
449 S::Future: Send + 'static,
450{
451 type Service = ErrorHandlerService<S>;
452
453 fn layer(&self, inner: S) -> Self::Service {
454 ErrorHandlerService {
455 inner,
456 dlc_producer: self.dlc_producer.clone(),
457 policies: self
458 .policies
459 .iter()
460 .map(|(p, prod)| (p.clone(), prod.clone()))
461 .collect(),
462 }
463 }
464}
465
466pub struct ErrorHandlerService<S> {
471 inner: S,
472 dlc_producer: Option<BoxProcessor>,
473 policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
474}
475
476impl<S: Clone> Clone for ErrorHandlerService<S> {
477 fn clone(&self) -> Self {
478 Self {
479 inner: self.inner.clone(),
480 dlc_producer: self.dlc_producer.clone(),
481 policies: self
482 .policies
483 .iter()
484 .map(|(p, prod)| (p.clone(), prod.clone()))
485 .collect(),
486 }
487 }
488}
489
490impl<S> ErrorHandlerService<S>
491where
492 S: Service<Exchange, Response = Exchange, Error = CamelError> + Send + Clone + 'static,
493 S::Future: Send + 'static,
494{
495 pub fn new(
497 inner: S,
498 dlc_producer: Option<BoxProcessor>,
499 policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
500 ) -> Self {
501 Self {
502 inner,
503 dlc_producer,
504 policies,
505 }
506 }
507}
508
509impl<S> Service<Exchange> for ErrorHandlerService<S>
510where
511 S: Service<Exchange, Response = Exchange, Error = CamelError> + Send + Clone + 'static,
512 S::Future: Send + 'static,
513{
514 type Response = Exchange;
515 type Error = CamelError;
516 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
517
518 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
519 match self.inner.poll_ready(cx) {
524 Poll::Pending => Poll::Pending,
525 Poll::Ready(Err(_)) => Poll::Ready(Ok(())),
526 Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
527 }
528 }
529
530 fn call(&mut self, exchange: Exchange) -> Self::Future {
531 let mut inner = self.inner.clone();
532 let dlc = self.dlc_producer.clone();
533 let policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)> = self
534 .policies
535 .iter()
536 .map(|(p, prod)| (p.clone(), prod.clone()))
537 .collect();
538
539 Box::pin(async move {
540 let original = exchange.clone();
541 let result = match inner.ready().await {
542 Ok(svc) => svc.call(exchange).await,
543 Err(e) => Err(e), };
545
546 let err = match result {
547 Ok(ex) => return Ok(ex),
548 Err(e) => e,
549 };
550
551 let matched = policies.into_iter().find(|(p, _)| (p.matches)(&err));
553
554 if let Some((policy, policy_producer)) = matched {
555 if let Some(ref backoff) = policy.retry {
557 for attempt in 0..backoff.max_attempts {
558 let delay = backoff.delay_for(attempt);
559 tokio::time::sleep(delay).await;
560
561 let mut ex = original.clone();
563 ex.input.set_header(HEADER_REDELIVERED, Value::Bool(true));
564 ex.input.set_header(
565 HEADER_REDELIVERY_COUNTER,
566 Value::Number((attempt + 1).into()),
567 );
568 ex.input.set_header(
569 HEADER_REDELIVERY_MAX_COUNTER,
570 Value::Number(backoff.max_attempts.into()),
571 );
572
573 let result = match inner.ready().await {
574 Ok(svc) => svc.call(ex).await,
575 Err(e) => Err(e), };
577 match result {
578 Ok(ex) => return Ok(ex),
579 Err(retry_err) => {
580 if attempt + 1 == backoff.max_attempts {
581 let mut original = original.clone();
583 original
584 .input
585 .set_header(HEADER_REDELIVERED, Value::Bool(true));
586 original.input.set_header(
587 HEADER_REDELIVERY_COUNTER,
588 Value::Number(backoff.max_attempts.into()),
589 );
590 original.input.set_header(
591 HEADER_REDELIVERY_MAX_COUNTER,
592 Value::Number(backoff.max_attempts.into()),
593 );
594 if let Some(ref steps) = policy.on_steps {
595 let handler = policy_producer.clone().or(dlc.clone());
596 return execute_on_steps(
597 original,
598 retry_err,
599 steps,
600 policy.disposition,
601 handler,
602 )
603 .await;
604 }
605 original.set_error(retry_err);
606 let handler = policy_producer.or(dlc);
607 return send_to_handler(original, handler).await;
608 }
609 }
610 }
611 }
612 }
613 if let Some(ref steps) = policy.on_steps {
615 let handler = policy_producer.or(dlc);
616 return execute_on_steps(original, err, steps, policy.disposition, handler)
617 .await;
618 }
619 let mut ex = original.clone();
620 ex.set_error(err);
621 let handler = policy_producer.or(dlc);
622 send_to_handler(ex, handler).await
623 } else {
624 let mut ex = original;
626 ex.set_error(err);
627 send_to_handler(ex, dlc).await
628 }
629 })
630 }
631}
632
633async fn send_to_handler(
634 exchange: Exchange,
635 producer: Option<BoxProcessor>,
636) -> Result<Exchange, CamelError> {
637 match producer {
638 None => {
639 tracing::error!(
641 error = ?exchange.error,
642 "Exchange failed with no error handler configured"
643 );
644 Ok(exchange)
645 }
646 Some(mut prod) => match prod.ready().await {
647 Err(e) => {
648 tracing::error!("DLC/handler not ready: {e}");
650 Ok(exchange)
651 }
652 Ok(svc) => match svc.call(exchange.clone()).await {
653 Ok(ex) => Ok(ex),
654 Err(e) => {
655 tracing::error!("DLC/handler call failed: {e}");
657 Ok(exchange)
659 }
660 },
661 },
662 }
663}
664
665#[cfg(test)]
666mod tests {
667 use super::*;
668 use camel_api::{
669 BoxProcessor, BoxProcessorExt, CamelError, Exchange, Message, OutcomePipeline,
670 OutcomeSegment, PipelineOutcome, RetryableStep, SyncBoxProcessor, Value,
671 error_handler::RedeliveryPolicy,
672 };
673 use std::future::Future;
674 use std::pin::Pin;
675 use std::sync::{
676 Arc,
677 atomic::{AtomicU32, Ordering},
678 };
679 use std::time::Duration;
680 use tower::ServiceExt;
681
682 fn make_exchange() -> Exchange {
683 Exchange::new(Message::new("test"))
684 }
685
686 fn failing_processor() -> BoxProcessor {
687 BoxProcessor::from_fn(|_ex| {
688 Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
689 })
690 }
691
692 fn ok_processor() -> BoxProcessor {
693 BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
694 }
695
696 fn fail_n_times(n: u32) -> BoxProcessor {
697 let count = Arc::new(AtomicU32::new(0));
698 BoxProcessor::from_fn(move |ex| {
699 let count = Arc::clone(&count);
700 Box::pin(async move {
701 let c = count.fetch_add(1, Ordering::SeqCst);
702 if c < n {
703 Err(CamelError::ProcessorError(format!("attempt {c}")))
704 } else {
705 Ok(ex)
706 }
707 })
708 })
709 }
710
711 #[tokio::test]
712 async fn test_ok_passthrough() {
713 let svc = ErrorHandlerService::new(ok_processor(), None, vec![]);
714 let result = svc.oneshot(make_exchange()).await;
715 assert!(result.is_ok());
716 assert!(!result.unwrap().has_error());
717 }
718
719 #[tokio::test]
720 async fn test_error_goes_to_dlc() {
721 let received = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
722 let received_clone = Arc::clone(&received);
723 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
724 let r = Arc::clone(&received_clone);
725 Box::pin(async move {
726 r.lock().unwrap().push(ex.clone());
727 Ok(ex)
728 })
729 });
730
731 let svc = ErrorHandlerService::new(failing_processor(), Some(dlc), vec![]);
732 let result = svc.oneshot(make_exchange()).await;
733 assert!(result.is_ok());
734 let ex = result.unwrap();
735 assert!(ex.has_error());
736 assert_eq!(received.lock().unwrap().len(), 1);
737 }
738
739 #[tokio::test]
740 async fn test_retry_recovers() {
741 let inner = fail_n_times(2);
742 let policy = ExceptionPolicy {
743 matches: Arc::new(|_| true),
744 retry: Some(RedeliveryPolicy {
745 max_attempts: 3,
746 initial_delay: Duration::from_millis(1),
747 multiplier: 1.0,
748 max_delay: Duration::from_millis(10),
749 jitter_factor: 0.0,
750 }),
751 handled_by: None,
752 on_steps: None,
753 disposition: ExceptionDisposition::Propagate,
754 };
755 let svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
756 let result = svc.oneshot(make_exchange()).await;
757 assert!(result.is_ok());
758 assert!(!result.unwrap().has_error());
759 }
760
761 #[tokio::test]
762 async fn test_retry_exhausted_goes_to_dlc() {
763 let inner = fail_n_times(10);
764 let received = Arc::new(std::sync::Mutex::new(0u32));
765 let received_clone = Arc::clone(&received);
766 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
767 let r = Arc::clone(&received_clone);
768 Box::pin(async move {
769 *r.lock().unwrap() += 1;
770 Ok(ex)
771 })
772 });
773 let policy = ExceptionPolicy {
774 matches: Arc::new(|_| true),
775 retry: Some(RedeliveryPolicy {
776 max_attempts: 2,
777 initial_delay: Duration::from_millis(1),
778 multiplier: 1.0,
779 max_delay: Duration::from_millis(10),
780 jitter_factor: 0.0,
781 }),
782 handled_by: None,
783 on_steps: None,
784 disposition: ExceptionDisposition::Propagate,
785 };
786 let svc = ErrorHandlerService::new(inner, Some(dlc), vec![(policy, None)]);
787 let result = svc.oneshot(make_exchange()).await;
788 assert!(result.is_ok());
789 assert!(result.unwrap().has_error());
790 assert_eq!(*received.lock().unwrap(), 1);
791 }
792
793 #[test]
794 fn test_poll_ready_delegates_to_inner() {
795 use std::sync::atomic::AtomicBool;
796
797 #[derive(Clone)]
799 struct DelayedReadyService {
800 ready: Arc<AtomicBool>,
801 }
802
803 impl Service<Exchange> for DelayedReadyService {
804 type Response = Exchange;
805 type Error = CamelError;
806 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
807
808 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
809 if self.ready.fetch_or(true, Ordering::SeqCst) {
810 Poll::Ready(Ok(()))
812 } else {
813 cx.waker().wake_by_ref();
815 Poll::Pending
816 }
817 }
818
819 fn call(&mut self, ex: Exchange) -> Self::Future {
820 Box::pin(async move { Ok(ex) })
821 }
822 }
823
824 let waker = futures::task::noop_waker();
825 let mut cx = Context::from_waker(&waker);
826
827 let inner = DelayedReadyService {
828 ready: Arc::new(AtomicBool::new(false)),
829 };
830 let mut svc = ErrorHandlerService::new(inner, None, vec![]);
831
832 let first = Pin::new(&mut svc).poll_ready(&mut cx);
834 assert!(first.is_pending(), "expected Pending on first poll_ready");
835
836 let second = Pin::new(&mut svc).poll_ready(&mut cx);
838 assert!(second.is_ready(), "expected Ready on second poll_ready");
839 }
840
841 #[tokio::test]
842 async fn test_no_matching_policy_uses_dlc() {
843 let received = Arc::new(std::sync::Mutex::new(0u32));
844 let received_clone = Arc::clone(&received);
845 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
846 let r = Arc::clone(&received_clone);
847 Box::pin(async move {
848 *r.lock().unwrap() += 1;
849 Ok(ex)
850 })
851 });
852 let policy = ExceptionPolicy::new(|e| matches!(e, CamelError::Io(_)));
853 let svc = ErrorHandlerService::new(failing_processor(), Some(dlc), vec![(policy, None)]);
854 let result = svc.oneshot(make_exchange()).await;
855 assert!(result.is_ok());
856 assert_eq!(*received.lock().unwrap(), 1);
857 }
858
859 #[tokio::test]
860 async fn test_redelivery_headers_are_set() {
861 use camel_api::error_handler::{
862 HEADER_REDELIVERED, HEADER_REDELIVERY_COUNTER, HEADER_REDELIVERY_MAX_COUNTER,
863 RedeliveryPolicy,
864 };
865
866 let inner = fail_n_times(10);
867 let received = Arc::new(std::sync::Mutex::new(None));
868 let received_clone = Arc::clone(&received);
869 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
870 let r = Arc::clone(&received_clone);
871 Box::pin(async move {
872 *r.lock().unwrap() = Some(ex.clone());
873 Ok(ex)
874 })
875 });
876
877 let policy = ExceptionPolicy {
878 matches: Arc::new(|_| true),
879 retry: Some(RedeliveryPolicy {
880 max_attempts: 2,
881 initial_delay: Duration::from_millis(1),
882 multiplier: 1.0,
883 max_delay: Duration::from_millis(10),
884 jitter_factor: 0.0,
885 }),
886 handled_by: None,
887 on_steps: None,
888 disposition: ExceptionDisposition::Propagate,
889 };
890
891 let svc = ErrorHandlerService::new(inner, Some(dlc), vec![(policy, None)]);
892 let _ = svc.oneshot(make_exchange()).await.unwrap();
893
894 let ex = received.lock().unwrap().take().unwrap();
895 assert_eq!(
896 ex.input.header(HEADER_REDELIVERED),
897 Some(&Value::Bool(true))
898 );
899 assert_eq!(
900 ex.input.header(HEADER_REDELIVERY_COUNTER),
901 Some(&Value::Number(2.into()))
902 );
903 assert_eq!(
904 ex.input.header(HEADER_REDELIVERY_MAX_COUNTER),
905 Some(&Value::Number(2.into()))
906 );
907 }
908
909 #[tokio::test]
910 async fn test_jitter_produces_varying_delays_in_retry_flow() {
911 use std::time::Instant;
912
913 let inner = fail_n_times(10);
914 let received = Arc::new(std::sync::Mutex::new(None));
915 let received_clone = Arc::clone(&received);
916 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
917 let r = Arc::clone(&received_clone);
918 Box::pin(async move {
919 *r.lock().unwrap() = Some(ex.clone());
920 Ok(ex)
921 })
922 });
923
924 let policy = ExceptionPolicy {
925 matches: Arc::new(|_| true),
926 retry: Some(RedeliveryPolicy {
927 max_attempts: 5,
928 initial_delay: Duration::from_millis(20),
929 multiplier: 1.0,
930 max_delay: Duration::from_millis(100),
931 jitter_factor: 0.5,
932 }),
933 handled_by: None,
934 on_steps: None,
935 disposition: ExceptionDisposition::Propagate,
936 };
937
938 let start = Instant::now();
939 let svc = ErrorHandlerService::new(inner, Some(dlc), vec![(policy, None)]);
940 let _ = svc.oneshot(make_exchange()).await.unwrap();
941 let elapsed = start.elapsed();
942
943 assert!(
944 received.lock().unwrap().is_some(),
945 "DLC should have received exchange"
946 );
947
948 assert!(
949 elapsed >= Duration::from_millis(50),
950 "5 retries with 20ms base delay should take at least 50ms (with jitter low bound)"
951 );
952
953 assert!(
954 elapsed <= Duration::from_millis(500),
955 "5 retries with 20ms base delay + 50% jitter should not exceed 500ms"
956 );
957 }
958
959 #[tokio::test]
960 async fn test_on_steps_handled_true_consumes_error() {
961 use tower::ServiceExt;
962
963 let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
964 ex.input.body = camel_api::Body::Bytes("handled".into());
965 async move { Ok(ex) }
966 }));
967 let policy = ExceptionPolicy {
968 matches: Arc::new(|_| true),
969 retry: None,
970 handled_by: None,
971 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
972 disposition: ExceptionDisposition::Handled,
973 };
974 let inner = tower::service_fn(|_ex: Exchange| async {
975 Err::<Exchange, CamelError>(CamelError::RouteError("fail".to_string()))
976 });
977 let mut svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
978 let ex = Exchange::default();
979 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
980 assert!(result.error.is_none(), "handled:true should clear error");
981 assert!(matches!(result.input.body, camel_api::Body::Bytes(_)));
982 }
983
984 #[tokio::test]
985 async fn test_on_steps_handled_false_propagates_error() {
986 use tower::ServiceExt;
987
988 let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
989 ex.input.body = camel_api::Body::Bytes("handled".into());
990 async move { Ok(ex) }
991 }));
992 let policy = ExceptionPolicy {
993 matches: Arc::new(|_| true),
994 retry: None,
995 handled_by: None,
996 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
997 disposition: ExceptionDisposition::Propagate,
998 };
999 let inner = tower::service_fn(|_ex: Exchange| async {
1000 Err::<Exchange, CamelError>(CamelError::RouteError("fail".to_string()))
1001 });
1002 let mut svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
1003 let ex = Exchange::default();
1004 let result = svc.ready().await.unwrap().call(ex).await;
1005 assert!(result.is_err(), "handled:false should propagate error");
1006 }
1007
1008 #[derive(Clone)]
1016 struct ReadinessFailService {
1017 error: CamelError,
1018 }
1019
1020 impl ReadinessFailService {
1021 fn new(error: CamelError) -> Self {
1022 Self { error }
1023 }
1024 }
1025
1026 impl Service<Exchange> for ReadinessFailService {
1027 type Response = Exchange;
1028 type Error = CamelError;
1029 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
1030
1031 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1032 Poll::Ready(Err(self.error.clone()))
1033 }
1034
1035 fn call(&mut self, ex: Exchange) -> Self::Future {
1036 Box::pin(async move { Ok(ex) })
1039 }
1040 }
1041
1042 #[tokio::test]
1043 async fn test_readiness_error_goes_to_dlc() {
1044 let readiness_err = CamelError::ProcessorError("readiness-fail".into());
1045 let inner = ReadinessFailService {
1046 error: readiness_err,
1047 };
1048
1049 let received = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
1050 let received_clone = Arc::clone(&received);
1051 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1052 let r = Arc::clone(&received_clone);
1053 Box::pin(async move {
1054 r.lock().unwrap().push(ex.clone());
1055 Ok(ex)
1056 })
1057 });
1058
1059 let svc = ErrorHandlerService::new(inner, Some(dlc), vec![]);
1060 let result = svc.oneshot(make_exchange()).await;
1061
1062 assert!(
1064 result.is_ok(),
1065 "readiness error should be captured and sent to DLC, got: {:?}",
1066 result
1067 );
1068 let ex = result.unwrap();
1069 assert!(ex.has_error(), "exchange should carry the readiness error");
1070 assert_eq!(
1071 received.lock().unwrap().len(),
1072 1,
1073 "DLC should have received the exchange exactly once"
1074 );
1075 }
1076
1077 #[tokio::test]
1078 async fn test_readiness_error_goes_to_matching_policy() {
1079 let readiness_err = CamelError::ProcessorError("readiness-fail".into());
1080 let inner = ReadinessFailService {
1081 error: readiness_err,
1082 };
1083
1084 let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
1085 ex.input.body = camel_api::Body::Bytes("handled-readiness".into());
1086 async move { Ok(ex) }
1087 }));
1088 let policy = ExceptionPolicy {
1089 matches: Arc::new(|_| true),
1090 retry: None,
1091 handled_by: None,
1092 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1093 disposition: ExceptionDisposition::Handled,
1094 };
1095
1096 let svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
1097 let result = svc.oneshot(make_exchange()).await;
1098
1099 assert!(
1101 result.is_ok(),
1102 "readiness error should be captured by policy, got: {:?}",
1103 result
1104 );
1105 let ex = result.unwrap();
1106 assert!(ex.error.is_none(), "handled:true should clear error");
1107 assert!(
1108 matches!(ex.input.body, camel_api::Body::Bytes(_)),
1109 "on_steps should have modified the body"
1110 );
1111 }
1112
1113 #[test]
1114 fn test_poll_ready_converts_readiness_error_to_ok() {
1115 let readiness_err = CamelError::ProcessorError("readiness-fail".into());
1116 let inner = ReadinessFailService {
1117 error: readiness_err,
1118 };
1119 let mut svc = ErrorHandlerService::new(inner, None, vec![]);
1120
1121 let waker = futures::task::noop_waker();
1122 let mut cx = Context::from_waker(&waker);
1123
1124 let poll = Pin::new(&mut svc).poll_ready(&mut cx);
1126 match poll {
1127 Poll::Ready(Ok(())) => { }
1128 Poll::Ready(Err(e)) => panic!("poll_ready leaked readiness error: {:?}", e),
1129 Poll::Pending => panic!("poll_ready should be Ready for readiness errors"),
1130 }
1131 }
1132
1133 #[tokio::test]
1136 async fn test_invoke_processor_returns_ok_on_success() {
1137 let mut svc = ok_processor();
1138 let ex = make_exchange();
1139 let result = invoke_processor(&mut svc, ex).await;
1140 assert!(result.is_ok());
1141 }
1142
1143 #[tokio::test]
1144 async fn test_invoke_processor_captures_readiness_error() {
1145 let mut failing_ready: BoxProcessor = BoxProcessor::new(ReadinessFailService::new(
1146 CamelError::ProcessorError("not ready".into()),
1147 ));
1148 let ex = make_exchange();
1149 let result = invoke_processor(&mut failing_ready, ex).await;
1150 assert!(result.is_err());
1151 }
1152
1153 #[tokio::test]
1154 async fn test_on_steps_handled_true_clears_exception_properties() {
1155 use tower::ServiceExt;
1156
1157 let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
1158 ex.input.body = camel_api::Body::Bytes("handled".into());
1159 async move { Ok(ex) }
1160 }));
1161 let policy = ExceptionPolicy {
1162 matches: Arc::new(|_| true),
1163 retry: None,
1164 handled_by: None,
1165 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1166 disposition: ExceptionDisposition::Handled,
1167 };
1168 let inner = tower::service_fn(|_ex: Exchange| async {
1169 Err::<Exchange, CamelError>(CamelError::RouteError("fail".to_string()))
1170 });
1171 let mut svc = ErrorHandlerService::new(inner, None, vec![(policy, None)]);
1172 let ex = Exchange::default();
1173 let result = svc.ready().await.unwrap().call(ex).await.unwrap();
1174 assert!(result.error.is_none(), "handled:true should clear error");
1175 assert!(
1176 !result
1177 .properties
1178 .contains_key(camel_api::exchange::PROPERTY_EXCEPTION_MESSAGE),
1179 "handled:true should clear exception properties"
1180 );
1181 assert!(
1182 !result
1183 .properties
1184 .contains_key(camel_api::exchange::PROPERTY_EXCEPTION_KIND),
1185 "handled:true should clear exception kind property"
1186 );
1187 assert!(
1188 !result
1189 .properties
1190 .contains_key(camel_api::exchange::PROPERTY_EXCEPTION_CAUGHT),
1191 "handled:true should clear exception caught property"
1192 );
1193 }
1194
1195 #[test]
1198 fn test_match_policy_returns_id_for_matching_error() {
1199 let handler = DefaultRouteErrorHandler::new(
1200 None,
1201 vec![(
1202 ExceptionPolicy::new(|e| matches!(e, CamelError::ProcessorError(_))),
1203 None,
1204 )],
1205 );
1206 let id = handler.match_policy(&CamelError::ProcessorError("test".into()));
1207 assert_eq!(id, Some(PolicyId(0)));
1208 }
1209
1210 #[test]
1211 fn test_match_policy_returns_none_for_unmatched() {
1212 let handler = DefaultRouteErrorHandler::new(None, vec![]);
1213 let id = handler.match_policy(&CamelError::ProcessorError("test".into()));
1214 assert_eq!(id, None);
1215 }
1216
1217 #[tokio::test]
1220 async fn test_retry_step_succeeds_on_second_attempt() {
1221 let mut policy = ExceptionPolicy::new(|_| true);
1222 policy.retry = Some(RedeliveryPolicy::new(3));
1223 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1224 let mut step = fail_n_times(1); let ex = make_exchange();
1226 let outcome = handler
1227 .retry_step(
1228 Some(PolicyId(0)),
1229 &mut step,
1230 ex,
1231 CamelError::ProcessorError("attempt 0".into()),
1232 )
1233 .await;
1234 assert!(matches!(outcome, RetryOutcome::Recovered(_)));
1235 }
1236
1237 #[tokio::test]
1238 async fn test_retry_step_exhausted_when_all_fail() {
1239 let mut policy = ExceptionPolicy::new(|_| true);
1240 policy.retry = Some(RedeliveryPolicy::new(3));
1241 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1242 let mut step = failing_processor();
1243 let ex = make_exchange();
1244 let outcome = handler
1245 .retry_step(
1246 Some(PolicyId(0)),
1247 &mut step,
1248 ex,
1249 CamelError::ProcessorError("boom".into()),
1250 )
1251 .await;
1252 assert!(matches!(outcome, RetryOutcome::Exhausted { .. }));
1253 }
1254
1255 #[tokio::test]
1256 async fn test_retry_step_no_policy_returns_exhausted_immediately() {
1257 let handler = DefaultRouteErrorHandler::new(None, vec![]);
1258 let mut step = ok_processor();
1259 let ex = make_exchange();
1260 let outcome = handler
1261 .retry_step(
1262 None,
1263 &mut step,
1264 ex,
1265 CamelError::ProcessorError("boom".into()),
1266 )
1267 .await;
1268 assert!(matches!(
1269 outcome,
1270 RetryOutcome::Exhausted { policy: None, .. }
1271 ));
1272 }
1273
1274 #[tokio::test]
1277 async fn test_handle_step_propagate_sends_to_dlc() {
1278 let dlc = BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }));
1279 let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1280 let ex = make_exchange();
1281 let result = handler
1282 .handle_step(None, ex, CamelError::ProcessorError("boom".into()))
1283 .await;
1284 assert!(matches!(result, Ok(StepDisposition::Propagate(_))));
1285 }
1286
1287 #[tokio::test]
1288 async fn test_handle_step_handled_uses_handler_output() {
1289 let handler_producer = BoxProcessor::from_fn(|mut ex| {
1290 Box::pin(async move {
1291 ex.input.set_header("processed_by", Value::Bool(true));
1292 Ok(ex)
1293 })
1294 });
1295 let policy = ExceptionPolicy {
1296 matches: std::sync::Arc::new(|_| true),
1297 retry: None,
1298 handled_by: None,
1299 on_steps: None,
1300 disposition: ExceptionDisposition::Handled,
1301 };
1302 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, Some(handler_producer))]);
1303 let mut ex = make_exchange();
1304 ex.set_error(CamelError::ProcessorError("boom".into()));
1305 let result = handler
1306 .handle_step(
1307 Some(PolicyId(0)),
1308 ex,
1309 CamelError::ProcessorError("boom".into()),
1310 )
1311 .await;
1312 match result {
1313 Ok(StepDisposition::Handled(ex)) => {
1314 assert!(!ex.has_error(), "error should be cleared");
1315 assert_eq!(
1316 ex.input.header("processed_by"),
1317 Some(&Value::Bool(true)),
1318 "should use handler's output exchange"
1319 );
1320 }
1321 other => panic!("expected Handled, got {:?}", other.is_ok()),
1322 }
1323 }
1324
1325 #[tokio::test]
1326 async fn test_handle_step_continued_clears_error() {
1327 let policy = ExceptionPolicy {
1328 matches: std::sync::Arc::new(|_| true),
1329 retry: None,
1330 handled_by: None,
1331 on_steps: None,
1332 disposition: ExceptionDisposition::Continued,
1333 };
1334 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1335 let mut ex = make_exchange();
1336 ex.set_error(CamelError::ProcessorError("boom".into()));
1337 let result = handler
1338 .handle_step(
1339 Some(PolicyId(0)),
1340 ex,
1341 CamelError::ProcessorError("boom".into()),
1342 )
1343 .await;
1344 match result {
1345 Ok(StepDisposition::Continued(ex)) => assert!(!ex.has_error()),
1346 other => panic!("expected Continued, got {:?}", other.is_ok()),
1347 }
1348 }
1349
1350 #[tokio::test]
1351 async fn test_handle_step_with_on_steps_handled() {
1352 let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
1353 ex.input.body = camel_api::Body::Bytes("on_steps_ran".into());
1354 async move { Ok(ex) }
1355 }));
1356 let policy = ExceptionPolicy {
1357 matches: std::sync::Arc::new(|_| true),
1358 retry: None,
1359 handled_by: None,
1360 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1361 disposition: ExceptionDisposition::Handled,
1362 };
1363 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1364 let mut ex = make_exchange();
1365 ex.set_error(CamelError::ProcessorError("boom".into()));
1366 let result = handler
1367 .handle_step(
1368 Some(PolicyId(0)),
1369 ex,
1370 CamelError::ProcessorError("boom".into()),
1371 )
1372 .await;
1373 match result {
1374 Ok(StepDisposition::Handled(ex)) => {
1375 assert!(!ex.has_error(), "error should be cleared");
1376 assert!(
1377 matches!(ex.input.body, camel_api::Body::Bytes(_)),
1378 "on_steps should have modified the body"
1379 );
1380 }
1381 other => panic!("expected Handled, got: {}", other.is_ok()),
1382 }
1383 }
1384
1385 #[tokio::test]
1386 async fn test_handle_step_dlc_failure_propagates() {
1387 let failing_dlc = BoxProcessor::from_fn(|_| {
1388 Box::pin(async { Err(CamelError::ProcessorError("dlc broken".into())) })
1389 });
1390 let handler = DefaultRouteErrorHandler::new(Some(failing_dlc), vec![]);
1391 let ex = make_exchange();
1392 let result = handler
1393 .handle_step(None, ex, CamelError::ProcessorError("original".into()))
1394 .await;
1395 assert!(
1396 matches!(result, Ok(StepDisposition::Propagate(_))),
1397 "DLC failure should still return Propagate with original error"
1398 );
1399 }
1400
1401 #[tokio::test]
1404 async fn test_handle_boundary_security_error_goes_to_dlc() {
1405 let dlc_count = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
1406 let count_clone = dlc_count.clone();
1407 let dlc = BoxProcessor::from_fn(move |ex| {
1408 let c = count_clone.clone();
1409 Box::pin(async move {
1410 c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1411 Ok(ex)
1412 })
1413 });
1414 let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1415 let ex = make_exchange();
1416 let result = handler
1417 .handle_boundary(
1418 BoundaryKind::Security,
1419 ex,
1420 CamelError::Unauthorized("denied".into()),
1421 )
1422 .await;
1423 assert!(result.is_ok());
1424 assert_eq!(dlc_count.load(std::sync::atomic::Ordering::SeqCst), 1);
1425 }
1426
1427 #[tokio::test]
1428 async fn test_handle_boundary_handled_clears_error() {
1429 let policy = ExceptionPolicy {
1430 matches: std::sync::Arc::new(|_| true),
1431 retry: None,
1432 handled_by: None,
1433 on_steps: None,
1434 disposition: ExceptionDisposition::Handled,
1435 };
1436 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1437 let ex = make_exchange();
1438 let result = handler
1439 .handle_boundary(
1440 BoundaryKind::Security,
1441 ex,
1442 CamelError::Unauthorized("denied".into()),
1443 )
1444 .await;
1445 assert!(result.is_ok());
1446 assert!(
1447 !result.unwrap().has_error(),
1448 "Handled disposition should clear error"
1449 );
1450 }
1451
1452 #[tokio::test]
1453 async fn test_handle_boundary_propagate_preserves_error() {
1454 let policy = ExceptionPolicy {
1455 matches: std::sync::Arc::new(|_| true),
1456 retry: None,
1457 handled_by: None,
1458 on_steps: None,
1459 disposition: ExceptionDisposition::Propagate,
1460 };
1461 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1462 let ex = make_exchange();
1463 let result = handler
1464 .handle_boundary(
1465 BoundaryKind::CircuitBreaker,
1466 ex,
1467 CamelError::CircuitOpen("open".into()),
1468 )
1469 .await;
1470 assert!(result.is_ok(), "boundary errors always return Ok");
1471 assert!(
1472 result.unwrap().has_error(),
1473 "Propagate disposition should preserve error"
1474 );
1475 }
1476
1477 #[tokio::test]
1478 async fn test_handle_boundary_continued_preserves_error_like_propagate() {
1479 let dlc_count = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
1480 let count_clone = dlc_count.clone();
1481 let dlc = BoxProcessor::from_fn(move |ex| {
1482 let c = count_clone.clone();
1483 Box::pin(async move {
1484 c.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1485 Ok(ex)
1486 })
1487 });
1488 let policy = ExceptionPolicy {
1489 matches: std::sync::Arc::new(|_| true),
1490 retry: None,
1491 handled_by: None,
1492 on_steps: None,
1493 disposition: ExceptionDisposition::Continued,
1494 };
1495 let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![(policy, None)]);
1496 let ex = make_exchange();
1497 let result = handler
1498 .handle_boundary(
1499 BoundaryKind::Security,
1500 ex,
1501 CamelError::Unauthorized("denied".into()),
1502 )
1503 .await;
1504 assert!(result.is_ok(), "boundary errors always return Ok");
1505 assert!(
1506 result.unwrap().has_error(),
1507 "Continued at boundary should preserve error"
1508 );
1509 assert_eq!(
1510 dlc_count.load(std::sync::atomic::Ordering::SeqCst),
1511 1,
1512 "DLC should be called"
1513 );
1514 }
1515
1516 #[tokio::test]
1517 async fn test_handle_boundary_with_on_steps_handled() {
1518 let steps_pipeline = BoxProcessor::new(tower::service_fn(|mut ex: Exchange| {
1519 ex.input.body = camel_api::Body::Bytes("on_steps_ran".into());
1520 async move { Ok(ex) }
1521 }));
1522 let policy = ExceptionPolicy {
1523 matches: std::sync::Arc::new(|_| true),
1524 retry: None,
1525 handled_by: None,
1526 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1527 disposition: ExceptionDisposition::Handled,
1528 };
1529 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1530 let ex = make_exchange();
1531 let result = handler
1532 .handle_boundary(
1533 BoundaryKind::Security,
1534 ex,
1535 CamelError::Unauthorized("denied".into()),
1536 )
1537 .await;
1538 assert!(result.is_ok(), "boundary errors always return Ok");
1539 let ex = result.unwrap();
1540 assert!(!ex.has_error(), "Handled disposition should clear error");
1541 assert!(
1542 matches!(ex.input.body, camel_api::Body::Bytes(_)),
1543 "on_steps should have modified the body"
1544 );
1545 }
1546
1547 #[tokio::test]
1548 async fn retry_step_segment_stop_maps_to_retry_outcome_stopped() {
1549 use std::sync::Arc;
1550 use std::sync::atomic::{AtomicUsize, Ordering};
1551
1552 #[derive(Clone)]
1553 struct StoppingSegment {
1554 n: Arc<AtomicUsize>,
1555 }
1556 impl OutcomePipeline for StoppingSegment {
1557 fn clone_box(&self) -> Box<dyn OutcomePipeline> {
1558 Box::new(self.clone())
1559 }
1560 fn run<'a>(
1561 &'a mut self,
1562 ex: Exchange,
1563 ) -> Pin<Box<dyn Future<Output = PipelineOutcome> + Send + 'a>> {
1564 let n = self.n.clone();
1565 Box::pin(async move {
1566 n.fetch_add(1, Ordering::SeqCst);
1567 PipelineOutcome::Stopped(ex)
1568 })
1569 }
1570 }
1571
1572 let call_count = Arc::new(AtomicUsize::new(0));
1573 let seg = OutcomeSegment::new(Box::new(StoppingSegment {
1574 n: call_count.clone(),
1575 }));
1576 let mut retryable: Box<dyn RetryableStep> = Box::new(seg);
1577
1578 let mut policy = ExceptionPolicy::new(|_e: &CamelError| true);
1579 policy.retry = Some(RedeliveryPolicy::new(3));
1580 let handler = DefaultRouteErrorHandler::new(None, vec![(policy, None)]);
1581
1582 let original = Exchange::new(Message::new("retry-me"));
1583 let err = CamelError::ProcessorError("trigger retry".into());
1584 let outcome = handler
1585 .retry_step(Some(PolicyId(0)), retryable.as_mut(), original, err)
1586 .await;
1587
1588 assert!(
1589 matches!(outcome, RetryOutcome::Stopped(_)),
1590 "Segment Stop must map to RetryOutcome::Stopped, got {:?}",
1591 outcome
1592 );
1593 assert_eq!(
1594 call_count.load(Ordering::SeqCst),
1595 1,
1596 "Stop must short-circuit retry — only 1 invoke expected, got {}",
1597 call_count.load(Ordering::SeqCst)
1598 );
1599 }
1600
1601 #[tokio::test]
1602 async fn retry_step_new_signature_works_with_dlc_producer() {
1603 use std::sync::Arc;
1604 use std::sync::atomic::{AtomicUsize, Ordering};
1605
1606 #[derive(Clone)]
1607 struct CountingProducer {
1608 count: Arc<AtomicUsize>,
1609 succeed_on: usize,
1610 }
1611 impl tower::Service<Exchange> for CountingProducer {
1612 type Response = Exchange;
1613 type Error = CamelError;
1614 type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
1615 fn poll_ready(
1616 &mut self,
1617 _cx: &mut std::task::Context<'_>,
1618 ) -> std::task::Poll<Result<(), Self::Error>> {
1619 std::task::Poll::Ready(Ok(()))
1620 }
1621 fn call(&mut self, ex: Exchange) -> Self::Future {
1622 let n = self.count.fetch_add(1, Ordering::SeqCst);
1623 let succeed_on = self.succeed_on;
1624 Box::pin(async move {
1625 if n >= succeed_on {
1626 Ok(ex)
1627 } else {
1628 Err(CamelError::ProcessorError("retry".into()))
1629 }
1630 })
1631 }
1632 }
1633
1634 let count = Arc::new(AtomicUsize::new(0));
1635 let producer = CountingProducer {
1636 count: count.clone(),
1637 succeed_on: 2,
1638 };
1639 let sync_bp = SyncBoxProcessor::new(BoxProcessor::new(producer));
1640 let bp1 = sync_bp.clone_inner();
1641 let bp2 = sync_bp.clone_inner();
1642 let mut retryable1: Box<dyn RetryableStep> = Box::new(bp1);
1643 let mut retryable2: Box<dyn RetryableStep> = Box::new(bp2);
1644
1645 let ex = Exchange::new(Message::new("dlc"));
1646 let outcome1 = retryable1.invoke(ex.clone()).await;
1647 let outcome2 = retryable2.invoke(ex).await;
1648 assert!(matches!(outcome1, PipelineOutcome::Failed(_)));
1649 assert!(matches!(outcome2, PipelineOutcome::Failed(_)));
1650 assert_eq!(
1651 count.load(Ordering::SeqCst),
1652 2,
1653 "DLC producer must be invoked exactly twice through SyncBoxProcessor"
1654 );
1655 drop(retryable1);
1656 drop(retryable2);
1657 drop(sync_bp);
1658 }
1659
1660 #[tokio::test]
1663 async fn test_use_original_message_restores_body_before_dlc() {
1664 let dlc_received = Arc::new(std::sync::Mutex::new(None::<Exchange>));
1667 let dlc_received_clone = Arc::clone(&dlc_received);
1668 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1669 let r = Arc::clone(&dlc_received_clone);
1670 Box::pin(async move {
1671 *r.lock().unwrap() = Some(ex.clone());
1672 Ok(ex)
1673 })
1674 });
1675
1676 let mut handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1677 handler.use_original_message = true;
1678
1679 let mut ex = make_exchange();
1681 ex.input = Message::new("original-body");
1682
1683 let original: Arc<Message> = Arc::new(ex.input.clone());
1685 ex.set_extension(camel_api::ORIGINAL_MESSAGE_EXTENSION, original);
1686
1687 ex.input.body = camel_api::Body::Bytes("mutated-body".into());
1689
1690 let result = handler
1692 .handle_step(None, ex, CamelError::ProcessorError("boom".into()))
1693 .await;
1694 assert!(matches!(result, Ok(StepDisposition::Propagate(_))));
1695
1696 let received = dlc_received
1698 .lock()
1699 .unwrap()
1700 .take()
1701 .expect("DLC should have been called");
1702 let received_text = match &received.input.body {
1703 camel_api::Body::Text(s) => s.clone(),
1704 camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
1705 camel_api::Body::Json(v) => v.to_string(),
1706 _ => String::new(),
1707 };
1708 assert_eq!(
1709 received_text, "original-body",
1710 "DLC should receive original message body, not mutated version"
1711 );
1712 }
1713
1714 #[tokio::test]
1715 async fn test_use_original_message_handle_boundary_restores_before_dlc() {
1716 let dlc_received = Arc::new(std::sync::Mutex::new(None::<Exchange>));
1717 let dlc_received_clone = Arc::clone(&dlc_received);
1718 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1719 let r = Arc::clone(&dlc_received_clone);
1720 Box::pin(async move {
1721 *r.lock().unwrap() = Some(ex.clone());
1722 Ok(ex)
1723 })
1724 });
1725
1726 let mut handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1727 handler.use_original_message = true;
1728
1729 let mut ex = make_exchange();
1730 ex.input = Message::new("orig-boundary");
1731
1732 let original: Arc<Message> = Arc::new(ex.input.clone());
1733 ex.set_extension(camel_api::ORIGINAL_MESSAGE_EXTENSION, original);
1734
1735 ex.input.body = camel_api::Body::Bytes("mutated-boundary".into());
1737
1738 let result = handler
1739 .handle_boundary(
1740 BoundaryKind::Security,
1741 ex,
1742 CamelError::Unauthorized("denied".into()),
1743 )
1744 .await;
1745 assert!(result.is_ok());
1746
1747 let received = dlc_received
1748 .lock()
1749 .unwrap()
1750 .take()
1751 .expect("DLC should have been called");
1752 let received_text = match &received.input.body {
1753 camel_api::Body::Text(s) => s.clone(),
1754 camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
1755 camel_api::Body::Json(v) => v.to_string(),
1756 _ => String::new(),
1757 };
1758 assert_eq!(
1759 received_text, "orig-boundary",
1760 "handle_boundary should restore original message before sending to DLC"
1761 );
1762 }
1763
1764 #[tokio::test]
1765 async fn test_use_original_message_false_does_not_restore() {
1766 let dlc_received = Arc::new(std::sync::Mutex::new(None::<Exchange>));
1768 let dlc_received_clone = Arc::clone(&dlc_received);
1769 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1770 let r = Arc::clone(&dlc_received_clone);
1771 Box::pin(async move {
1772 *r.lock().unwrap() = Some(ex.clone());
1773 Ok(ex)
1774 })
1775 });
1776
1777 let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![]);
1778 let mut ex = make_exchange();
1781 ex.input = Message::new("original-body");
1782
1783 let original: Arc<Message> = Arc::new(ex.input.clone());
1785 ex.set_extension(camel_api::ORIGINAL_MESSAGE_EXTENSION, original);
1786
1787 ex.input.body = camel_api::Body::Bytes("mutated-body".into());
1789
1790 let result = handler
1791 .handle_step(None, ex, CamelError::ProcessorError("boom".into()))
1792 .await;
1793 assert!(matches!(result, Ok(StepDisposition::Propagate(_))));
1794
1795 let received = dlc_received
1796 .lock()
1797 .unwrap()
1798 .take()
1799 .expect("DLC should have been called");
1800 let received_text = match &received.input.body {
1801 camel_api::Body::Text(s) => s.clone(),
1802 camel_api::Body::Bytes(b) => String::from_utf8_lossy(b).to_string(),
1803 camel_api::Body::Json(v) => v.to_string(),
1804 _ => String::new(),
1805 };
1806 assert_eq!(
1807 received_text, "mutated-body",
1808 "When use_original_message=false, DLC should see the mutated body"
1809 );
1810 }
1811
1812 #[tokio::test]
1815 async fn propagate_skips_on_steps_in_handle_step() {
1816 let on_steps_called = Arc::new(AtomicU32::new(0));
1817 let on_steps_called_clone = on_steps_called.clone();
1818 let steps_pipeline = BoxProcessor::new(tower::service_fn(move |mut ex: Exchange| {
1819 let c = on_steps_called_clone.clone();
1820 Box::pin(async move {
1821 c.fetch_add(1, Ordering::SeqCst);
1822 ex.input.body = camel_api::Body::Bytes("on_steps_ran".into());
1823 Ok(ex)
1824 })
1825 }));
1826 let dlc_called = Arc::new(AtomicU32::new(0));
1827 let dlc_called_clone = dlc_called.clone();
1828 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1829 let c = dlc_called_clone.clone();
1830 Box::pin(async move {
1831 c.fetch_add(1, Ordering::SeqCst);
1832 Ok(ex)
1833 })
1834 });
1835 let policy = ExceptionPolicy {
1836 matches: std::sync::Arc::new(|_| true),
1837 retry: None,
1838 handled_by: None,
1839 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1840 disposition: ExceptionDisposition::Propagate,
1841 };
1842 let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![(policy, None)]);
1843 let mut ex = make_exchange();
1844 ex.set_error(CamelError::ProcessorError("boom".into()));
1845 let result = handler
1846 .handle_step(
1847 Some(PolicyId(0)),
1848 ex,
1849 CamelError::ProcessorError("boom".into()),
1850 )
1851 .await;
1852 assert!(
1853 matches!(result, Ok(StepDisposition::Propagate(_))),
1854 "Propagate disposition should return Propagate"
1855 );
1856 assert_eq!(
1857 on_steps_called.load(Ordering::SeqCst),
1858 0,
1859 "on_steps must NOT be called when disposition is Propagate (double side-effect bug)"
1860 );
1861 assert_eq!(
1862 dlc_called.load(Ordering::SeqCst),
1863 1,
1864 "DLC should still be called when disposition is Propagate"
1865 );
1866 }
1867
1868 #[tokio::test]
1869 async fn propagate_skips_on_steps_in_handle_boundary() {
1870 let on_steps_called = Arc::new(AtomicU32::new(0));
1871 let on_steps_called_clone = on_steps_called.clone();
1872 let steps_pipeline = BoxProcessor::new(tower::service_fn(move |mut ex: Exchange| {
1873 let c = on_steps_called_clone.clone();
1874 Box::pin(async move {
1875 c.fetch_add(1, Ordering::SeqCst);
1876 ex.input.body = camel_api::Body::Bytes("on_steps_ran".into());
1877 Ok(ex)
1878 })
1879 }));
1880 let dlc_called = Arc::new(AtomicU32::new(0));
1881 let dlc_called_clone = dlc_called.clone();
1882 let dlc = BoxProcessor::from_fn(move |ex: Exchange| {
1883 let c = dlc_called_clone.clone();
1884 Box::pin(async move {
1885 c.fetch_add(1, Ordering::SeqCst);
1886 Ok(ex)
1887 })
1888 });
1889 let policy = ExceptionPolicy {
1890 matches: std::sync::Arc::new(|_| true),
1891 retry: None,
1892 handled_by: None,
1893 on_steps: Some(SyncBoxProcessor::new(steps_pipeline)),
1894 disposition: ExceptionDisposition::Propagate,
1895 };
1896 let handler = DefaultRouteErrorHandler::new(Some(dlc), vec![(policy, None)]);
1897 let ex = make_exchange();
1898 let result = handler
1899 .handle_boundary(
1900 BoundaryKind::CircuitBreaker,
1901 ex,
1902 CamelError::CircuitOpen("open".into()),
1903 )
1904 .await;
1905 assert!(result.is_ok(), "boundary errors always return Ok");
1906 assert!(
1907 result.unwrap().has_error(),
1908 "Propagate disposition should preserve error"
1909 );
1910 assert_eq!(
1911 on_steps_called.load(Ordering::SeqCst),
1912 0,
1913 "on_steps must NOT be called when disposition is Propagate (double side-effect bug)"
1914 );
1915 assert_eq!(
1916 dlc_called.load(Ordering::SeqCst),
1917 1,
1918 "DLC should still be called when disposition is Propagate"
1919 );
1920 }
1921}