Skip to main content

camel_processor/
error_handler.rs

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                // Propagate or Continued — steps execute for side-effects (e.g. logging) but
38                // the modified exchange is discarded and the original error propagated.
39                Err(original_err)
40            }
41        }
42        Err(_) => {
43            // log-policy: handler-owned
44            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
52/// Invoke a processor: readiness check + call, unified into a single Result.
53///
54/// Readiness errors and call errors are both returned as `Err(CamelError)`,
55/// allowing the pipeline's recovery loop to handle them uniformly.
56pub 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/// Route-level error handler owning ALL error handling logic.
67///
68/// Single owner of DLC, retry, onException policies. Called from
69/// `RouteChannelService` (boundary errors) and `run_steps` (step errors).
70#[async_trait::async_trait]
71pub trait RouteErrorHandler: Send + Sync {
72    /// Match a policy for the given error. Called once before retry.
73    fn match_policy(&self, err: &CamelError) -> Option<PolicyId>;
74
75    /// Phase 1: Retry the failed step.
76    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    /// Phase 2: Determine step disposition after retry exhaustion.
85    async fn handle_step(
86        &self,
87        policy: Option<PolicyId>,
88        exchange: Exchange,
89        error: CamelError,
90    ) -> Result<StepDisposition, CamelError>;
91
92    /// Handle boundary (infrastructure) errors.
93    async fn handle_boundary(
94        &self,
95        kind: BoundaryKind,
96        exchange: Exchange,
97        error: CamelError,
98    ) -> Result<Exchange, CamelError>;
99}
100
101/// Default implementation of RouteErrorHandler.
102/// Owns DLC producer exclusively. Encapsulates retry/onException/DLC logic.
103///
104/// Uses `SyncBoxProcessor` internally so the handler is `Send + Sync` as required
105/// by the `RouteErrorHandler` trait.
106pub struct DefaultRouteErrorHandler {
107    pub(crate) dlc_producer: Option<SyncBoxProcessor>,
108    pub(crate) policies: Vec<(ExceptionPolicy, Option<SyncBoxProcessor>)>,
109    /// When true, restore the original Message (body and headers, pre-route, pre-mutation)
110    /// before forwarding to the DLC/handler. The original message is stashed by
111    /// `RouteChannelService::call` as the `ORIGINAL_MESSAGE_EXTENSION` extension.
112    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    /// Enable restoration of the original message before forwarding to DLC.
131    pub fn with_use_original_message(mut self, enabled: bool) -> Self {
132        self.use_original_message = enabled;
133        self
134    }
135
136    /// Restore the original message from the `CamelOriginalMessage` extension if
137    /// `use_original_message` is enabled. Called immediately before `send_to_handler`
138    /// dispatch in both `handle_step` and `handle_boundary`.
139    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    /// Resolve (disposition, producer) for a matched policy.
149    /// Shared by handle_step and handle_boundary.
150    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        // Run on_steps if present (using the SAME policy identified by PolicyId).
275        // Skip on_steps for Propagate disposition to prevent double side-effects:
276        // on_steps results would be discarded (snapshot restored), and the DLC
277        // handler fires next — causing duplicate message production.
278        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                    // Propagate and any future variant restore the snapshot
302                    // and fall through to the DLC/handler path.
303                    _ => {
304                        exchange = snapshot;
305                    }
306                },
307                Err(_) => {
308                    exchange = snapshot;
309                }
310            }
311        }
312
313        // No on_steps, on_steps failed, or Propagate — forward to DLC/handler.
314        // BIND the returned exchange (must use handler output).
315        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                // Propagate and any future variant forward the error.
330                _ => Ok(StepDisposition::Propagate(error)),
331            },
332            // Dead code by construction: send_to_handler always returns Ok.
333            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        // Boundary errors: match policy, run on_steps, forward to DLC.
344        // Disposition mapping:
345        //   Handled → clear error, return Ok(exchange)
346        //   Propagate | Continued → forward to DLC, return Ok(exchange_with_error)
347        //   (Continued at boundary = Propagate — no next step to continue to)
348        let policy = self.match_policy(&error);
349        let (disposition, producer) = self.resolve_producer(policy);
350
351        // Run on_steps if present (shared logic with handle_step).
352        // Skip on_steps for Propagate/Continued disposition to prevent double
353        // side-effects: on_steps results would be discarded (snapshot restored),
354        // and the DLC handler fires next — causing duplicate message production.
355        // At boundary level, Continued maps to Propagate semantics.
356        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                    // Propagate | Continued and any future variant restore the snapshot.
378                    _ => {
379                        exchange = snapshot;
380                    }
381                },
382                Err(_) => {
383                    exchange = snapshot;
384                }
385            }
386        }
387
388        // Forward to DLC/handler — BIND returned exchange
389        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                // Propagate | Continued and any future variant forward the error.
399                _ => {
400                    let mut ex = handler_ex;
401                    ex.set_error(error);
402                    Ok(ex)
403                }
404            },
405            // Dead code by construction: send_to_handler always returns Ok.
406            Err(e) => Err(e),
407        }
408    }
409}
410
411/// Tower Layer that wraps a pipeline with error handling behaviour.
412///
413/// Constructed with already-resolved producers; URI resolution happens in `camel-core`.
414pub struct ErrorHandlerLayer {
415    /// Resolved DLC producer (None = log only).
416    dlc_producer: Option<BoxProcessor>,
417    /// Policies with their resolved `handled_by` producers.
418    policies: Vec<(ExceptionPolicy, Option<BoxProcessor>)>,
419}
420
421impl ErrorHandlerLayer {
422    /// Create the layer with pre-resolved producers.
423    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
454/// Tower Service that absorbs pipeline errors by retrying and/or forwarding to a DLC.
455///
456/// `call` always returns `Ok` — errors are absorbed. The returned exchange will have
457/// `has_error() == true` if the pipeline ultimately failed.
458pub 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    /// Create the service directly (used in unit tests; in production use the Layer).
484    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        // Preserve backpressure (Pending) but never leak readiness errors upward.
508        // Readiness errors are deferred to call(), where they go through the same
509        // retry/onException/DLC path as call() errors. This is safe because call()
510        // re-checks readiness on a fresh inner clone via inner.ready().await.
511        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), // readiness error — enters retry/DLC/onException path below
532            };
533
534            let err = match result {
535                Ok(ex) => return Ok(ex),
536                Err(e) => e,
537            };
538
539            // Find the first matching policy.
540            let matched = policies.into_iter().find(|(p, _)| (p.matches)(&err));
541
542            if let Some((policy, policy_producer)) = matched {
543                // Retry if configured.
544                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                        // Set redelivery headers
550                        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), // readiness error — enters retry exhaustion path
564                        };
565                        match result {
566                            Ok(ex) => return Ok(ex),
567                            Err(retry_err) => {
568                                if attempt + 1 == backoff.max_attempts {
569                                    // Retries exhausted — send to handler.
570                                    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                // No retry configured (or 0 attempts) — send to policy handler or DLC.
602                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                // No matching policy — forward directly to DLC.
613                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            // log-policy: system-broken
628            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                // log-policy: system-broken
637                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                    // log-policy: system-broken
644                    tracing::error!("DLC/handler call failed: {e}");
645                    // Return the original exchange with original error intact.
646                    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        /// A service that returns `Pending` on the first `poll_ready`, then `Ready`.
786        #[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                    // Already marked ready (second+ call) → Ready
799                    Poll::Ready(Ok(()))
800                } else {
801                    // First call → Pending, schedule a wake
802                    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        // First poll_ready: inner returns Pending, so ErrorHandlerService must too.
821        let first = Pin::new(&mut svc).poll_ready(&mut cx);
822        assert!(first.is_pending(), "expected Pending on first poll_ready");
823
824        // Second poll_ready: inner returns Ready, so ErrorHandlerService must too.
825        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    // --- Readiness error capture tests ---
997    //
998    // ErrorHandlerService must capture readiness errors (poll_ready returning Err)
999    // and route them through retry/onException/DLC instead of propagating raw.
1000
1001    /// A service whose `poll_ready` always returns `Ready(Err(...))` but whose
1002    /// `call` returns `Ok`. This simulates a permanently-not-ready endpoint.
1003    #[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            // call() should never be reached if poll_ready returns Err,
1025            // but Tower's ready().await on a clone will re-encounter the readiness error.
1026            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        // The error must be absorbed (Ok), not propagated raw (Err).
1051        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        // The error must be absorbed and routed to the on_steps handler.
1088        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        // poll_ready must NOT propagate the readiness error — convert to Ok.
1113        let poll = Pin::new(&mut svc).poll_ready(&mut cx);
1114        match poll {
1115            Poll::Ready(Ok(())) => { /* correct */ }
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    // --- invoke_processor tests ---
1122
1123    #[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    // ── DefaultRouteErrorHandler tests ──
1184
1185    #[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    // ── retry_step tests ──
1206
1207    #[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); // fails once, then succeeds
1213        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    // ── handle_step tests ──
1263
1264    #[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    // ── handle_boundary tests ──
1390
1391    #[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    // ── use_original_message tests ──
1649
1650    #[tokio::test]
1651    async fn test_use_original_message_restores_body_before_dlc() {
1652        // Verifies that when the extension is set, handle_step restores the
1653        // original message body before the DLC sees it.
1654        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        // Build exchange with original body, stash it, then mutate.
1668        let mut ex = make_exchange();
1669        ex.input = Message::new("original-body");
1670
1671        // Simulate RouteChannelService stashing the original message.
1672        let original: Arc<Message> = Arc::new(ex.input.clone());
1673        ex.set_extension(camel_api::ORIGINAL_MESSAGE_EXTENSION, original);
1674
1675        // Mutate the body (simulating a pipeline step that transforms then fails).
1676        ex.input.body = camel_api::Body::Bytes("mutated-body".into());
1677
1678        // Call handle_step — the restore should fire before send_to_handler.
1679        let result = handler
1680            .handle_step(None, ex, CamelError::ProcessorError("boom".into()))
1681            .await;
1682        assert!(matches!(result, Ok(StepDisposition::Propagate(_))));
1683
1684        // The DLC must have received the exchange with the ORIGINAL body.
1685        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        // Mutate body before boundary error.
1724        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        // When use_original_message is false (default), the mutation should PASS THROUGH.
1755        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        // use_original_message defaults to false
1767
1768        let mut ex = make_exchange();
1769        ex.input = Message::new("original-body");
1770
1771        // Stash still set but flag is false — must be ignored.
1772        let original: Arc<Message> = Arc::new(ex.input.clone());
1773        ex.set_extension(camel_api::ORIGINAL_MESSAGE_EXTENSION, original);
1774
1775        // Mutate the body.
1776        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    // ── D-M17: Propagate must skip on_steps (prevents double side-effects) ──
1801
1802    #[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}