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