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