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