Skip to main content

temporalio_common_wasm/
error.rs

1//! Shared error types used across Temporal SDK crates.
2
3use crate::{
4    WorkflowExecution,
5    data_converters::{
6        DecodablePayloads, GenericPayloadConverter, PayloadConversionError, PayloadConverter,
7        RawValue, SerializationContext, SerializationContextData, TemporalDeserializable,
8        TemporalSerializable,
9    },
10    protos::{
11        coresdk::child_workflow::StartChildWorkflowExecutionFailedCause,
12        temporal::api::{
13            common::v1::{Payload, Payloads},
14            enums::v1::{
15                ApplicationErrorCategory as ProtoApplicationErrorCategory,
16                RetryState as ProtoRetryState, TimeoutType as ProtoTimeoutType,
17            },
18            failure::v1::Failure,
19        },
20    },
21};
22use std::time::Duration;
23
24/// Describes why a retry did or did not occur.
25#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
26#[non_exhaustive]
27pub enum RetryState {
28    /// No retry state was specified.
29    Unspecified,
30    /// Another retry is in progress.
31    InProgress,
32    /// The failure is not retryable.
33    NonRetryableFailure,
34    /// The retry timed out.
35    Timeout,
36    /// The retry policy's maximum attempts were reached.
37    MaximumAttemptsReached,
38    /// No retry policy was configured.
39    RetryPolicyNotSet,
40    /// An internal server error prevented retrying.
41    InternalServerError,
42    /// Cancellation was requested.
43    CancelRequested,
44    /// A state introduced by a newer server or API version.
45    Unknown,
46}
47
48impl RetryState {
49    fn from_raw(value: i32) -> Self {
50        match ProtoRetryState::try_from(value) {
51            Ok(ProtoRetryState::Unspecified) => Self::Unspecified,
52            Ok(ProtoRetryState::InProgress) => Self::InProgress,
53            Ok(ProtoRetryState::NonRetryableFailure) => Self::NonRetryableFailure,
54            Ok(ProtoRetryState::Timeout) => Self::Timeout,
55            Ok(ProtoRetryState::MaximumAttemptsReached) => Self::MaximumAttemptsReached,
56            Ok(ProtoRetryState::RetryPolicyNotSet) => Self::RetryPolicyNotSet,
57            Ok(ProtoRetryState::InternalServerError) => Self::InternalServerError,
58            Ok(ProtoRetryState::CancelRequested) => Self::CancelRequested,
59            Err(_) => Self::Unknown,
60        }
61    }
62}
63
64impl From<ProtoRetryState> for RetryState {
65    fn from(value: ProtoRetryState) -> Self {
66        Self::from_raw(value as i32)
67    }
68}
69
70/// Identifies which timeout expired.
71#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
72#[non_exhaustive]
73pub enum TimeoutType {
74    /// No timeout type was specified.
75    Unspecified,
76    /// The execution exceeded its start-to-close timeout.
77    StartToClose,
78    /// The task exceeded its schedule-to-start timeout.
79    ScheduleToStart,
80    /// The execution exceeded its schedule-to-close timeout.
81    ScheduleToClose,
82    /// An activity heartbeat was not received in time.
83    Heartbeat,
84    /// A timeout introduced by a newer server or API version.
85    Unknown,
86}
87
88impl TimeoutType {
89    fn from_raw(value: i32) -> Self {
90        match ProtoTimeoutType::try_from(value) {
91            Ok(ProtoTimeoutType::Unspecified) => Self::Unspecified,
92            Ok(ProtoTimeoutType::StartToClose) => Self::StartToClose,
93            Ok(ProtoTimeoutType::ScheduleToStart) => Self::ScheduleToStart,
94            Ok(ProtoTimeoutType::ScheduleToClose) => Self::ScheduleToClose,
95            Ok(ProtoTimeoutType::Heartbeat) => Self::Heartbeat,
96            Err(_) => Self::Unknown,
97        }
98    }
99}
100
101impl From<ProtoTimeoutType> for TimeoutType {
102    fn from(value: ProtoTimeoutType) -> Self {
103        Self::from_raw(value as i32)
104    }
105}
106
107// We cannot store `Box<dyn TemporalSerializable>` directly here because erased values still need
108// to be driven back through the active `PayloadConverter` to reach serde-based implementations.
109trait SerializableFailurePayload: Send + Sync {
110    fn to_payloads(
111        &self,
112        payload_converter: &PayloadConverter,
113        context: &SerializationContextData,
114    ) -> Result<Vec<Payload>, PayloadConversionError>;
115}
116
117impl<T> SerializableFailurePayload for T
118where
119    T: TemporalSerializable + Send + Sync + 'static,
120{
121    fn to_payloads(
122        &self,
123        payload_converter: &PayloadConverter,
124        context: &SerializationContextData,
125    ) -> Result<Vec<Payload>, PayloadConversionError> {
126        payload_converter.to_payloads(
127            &SerializationContext {
128                data: context,
129                converter: payload_converter,
130            },
131            self,
132        )
133    }
134}
135
136/// Payloads attached to a failure, either as a deferred outbound value or decoded inbound payloads.
137#[derive(derive_more::Debug)]
138pub struct FailurePayloads {
139    repr: FailurePayloadsRepr,
140}
141
142#[derive(derive_more::Debug)]
143enum FailurePayloadsRepr {
144    #[debug("Serializable(...)")]
145    Serializable(#[debug(skip)] Box<dyn SerializableFailurePayload>),
146    Decoded(DecodablePayloads),
147}
148
149impl FailurePayloads {
150    pub(crate) fn encode(
151        &self,
152        payload_converter: &PayloadConverter,
153        context: &SerializationContextData,
154    ) -> Result<Payloads, PayloadConversionError> {
155        let payloads = match &self.repr {
156            FailurePayloadsRepr::Serializable(value) => {
157                value.to_payloads(payload_converter, context)?
158            }
159            FailurePayloadsRepr::Decoded(value) => value.raw().to_vec(),
160        };
161        Ok(Payloads { payloads })
162    }
163
164    /// Deserialize the decoded payloads into a typed value.
165    pub fn deserialize<T: TemporalDeserializable + 'static>(
166        &self,
167    ) -> Result<T, PayloadConversionError> {
168        match &self.repr {
169            FailurePayloadsRepr::Decoded(value) => value.deserialize(),
170            FailurePayloadsRepr::Serializable(_) => Err(PayloadConversionError::WrongEncoding),
171        }
172    }
173
174    /// Returns the decoded raw payloads, if present.
175    pub fn raw(&self) -> Option<&[Payload]> {
176        match &self.repr {
177            FailurePayloadsRepr::Decoded(value) => Some(value.raw()),
178            FailurePayloadsRepr::Serializable(_) => None,
179        }
180    }
181
182    /// Consume this value and return the decoded payloads as a [`RawValue`], if present.
183    pub fn into_raw(self) -> Option<RawValue> {
184        match self.repr {
185            FailurePayloadsRepr::Decoded(value) => Some(value.into_raw()),
186            FailurePayloadsRepr::Serializable(_) => None,
187        }
188    }
189}
190
191impl From<DecodablePayloads> for FailurePayloads {
192    fn from(value: DecodablePayloads) -> Self {
193        Self {
194            repr: FailurePayloadsRepr::Decoded(value),
195        }
196    }
197}
198
199impl<T> From<T> for FailurePayloads
200where
201    T: TemporalSerializable + Send + Sync + 'static,
202{
203    fn from(value: T) -> Self {
204        Self {
205            repr: FailurePayloadsRepr::Serializable(Box::new(value)),
206        }
207    }
208}
209
210/// Categorizes an [`ApplicationFailure`] to hint at how the server and tooling should treat it.
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
212#[non_exhaustive]
213pub enum ApplicationErrorCategory {
214    /// No category was specified.
215    #[default]
216    Unspecified,
217    /// An expected error with little or no severity. Benign errors are logged at a reduced level
218    /// and excluded from error metrics by the server.
219    Benign,
220}
221
222impl From<ApplicationErrorCategory> for ProtoApplicationErrorCategory {
223    fn from(value: ApplicationErrorCategory) -> Self {
224        match value {
225            ApplicationErrorCategory::Unspecified => ProtoApplicationErrorCategory::Unspecified,
226            ApplicationErrorCategory::Benign => ProtoApplicationErrorCategory::Benign,
227        }
228    }
229}
230
231impl From<ProtoApplicationErrorCategory> for ApplicationErrorCategory {
232    fn from(value: ProtoApplicationErrorCategory) -> Self {
233        match value {
234            ProtoApplicationErrorCategory::Unspecified => ApplicationErrorCategory::Unspecified,
235            ProtoApplicationErrorCategory::Benign => ApplicationErrorCategory::Benign,
236        }
237    }
238}
239
240/// User-authored application failure metadata that can be converted into a Temporal failure.
241#[derive(Debug, bon::Builder)]
242#[builder(start_fn = builder, state_mod(vis = "pub"))]
243pub struct ApplicationFailure {
244    #[builder(start_fn, into)]
245    source: Box<dyn std::error::Error + Send + Sync>,
246    type_name: Option<String>,
247    #[builder(default)]
248    non_retryable: bool,
249    next_retry_delay: Option<Duration>,
250    #[builder(default = ApplicationErrorCategory::Unspecified)]
251    category: ApplicationErrorCategory,
252    #[builder(into)]
253    details: Option<FailurePayloads>,
254    failure: Option<Failure>,
255    cause: Option<Box<IncomingError>>,
256}
257
258impl ApplicationFailure {
259    /// Construct a retryable application failure with no extra metadata.
260    pub fn new(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
261        Self {
262            source: source.into(),
263            type_name: None,
264            non_retryable: false,
265            next_retry_delay: None,
266            category: ApplicationErrorCategory::Unspecified,
267            details: None,
268            failure: None,
269            cause: None,
270        }
271    }
272
273    /// Construct a non-retryable application failure with no extra metadata.
274    pub fn non_retryable(source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
275        Self {
276            non_retryable: true,
277            ..Self::new(source)
278        }
279    }
280
281    /// Returns the wrapped source error.
282    pub fn source_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
283        &*self.source as &(dyn std::error::Error + Send + Sync + 'static)
284    }
285
286    /// Returns the configured application failure type name, if any.
287    pub fn type_name(&self) -> Option<&str> {
288        self.type_name.as_deref()
289    }
290
291    /// Returns true if this failure should be treated as non-retryable.
292    pub fn is_non_retryable(&self) -> bool {
293        self.non_retryable
294    }
295
296    /// Returns the explicitly configured next retry delay, if any.
297    pub fn next_retry_delay(&self) -> Option<Duration> {
298        self.next_retry_delay
299    }
300
301    /// Returns the application error category.
302    pub fn category(&self) -> ApplicationErrorCategory {
303        self.category
304    }
305
306    /// Returns the decoded details deserialized as the requested type, if any.
307    pub fn details<T: TemporalDeserializable + 'static>(
308        &self,
309    ) -> Result<Option<T>, PayloadConversionError> {
310        self.details
311            .as_ref()
312            .map(FailurePayloads::deserialize)
313            .transpose()
314    }
315
316    /// Returns the raw decoded details payloads, if any.
317    pub fn raw_details(&self) -> Option<&[Payload]> {
318        self.details.as_ref().and_then(FailurePayloads::raw)
319    }
320
321    pub(crate) fn failure_payloads(&self) -> Option<&FailurePayloads> {
322        self.details.as_ref()
323    }
324
325    /// Returns the original failure proto when this application failure was decoded from one.
326    pub fn failure(&self) -> Option<&Failure> {
327        self.failure.as_ref()
328    }
329
330    /// Consumes this application failure and returns the retained proto failure, if one exists.
331    pub fn into_failure(self) -> Option<Failure> {
332        self.failure
333    }
334
335    /// Returns the normalized cause, if any.
336    pub fn cause(&self) -> Option<&IncomingError> {
337        self.cause.as_deref()
338    }
339
340    /// If this [`ApplicationFailure`] was caused by a timeout, returns the associated
341    /// [`TimeoutError`].
342    pub fn as_timeout(&self) -> Option<&TimeoutError> {
343        self.cause().and_then(IncomingError::as_timeout)
344    }
345
346    /// If this [`ApplicationFailure`] was caused by a cancellation, returns the associated
347    /// [`CancelledError`].
348    pub fn as_cancelled(&self) -> Option<&CancelledError> {
349        self.cause().and_then(IncomingError::as_cancelled)
350    }
351
352    pub(crate) fn from_failure(
353        failure: Failure,
354        cause: Option<IncomingError>,
355        payload_converter: &PayloadConverter,
356        context: &SerializationContextData,
357    ) -> Self {
358        let app_info = failure
359            .maybe_application_failure()
360            .cloned()
361            .unwrap_or_default();
362        let type_name = (!app_info.r#type.is_empty()).then_some(app_info.r#type.clone());
363        Self {
364            source: failure.message.clone().into(),
365            type_name,
366            non_retryable: app_info.non_retryable,
367            next_retry_delay: app_info.next_retry_delay.and_then(|d| d.try_into().ok()),
368            category: app_info.category().into(),
369            details: app_info.details.map(|details| {
370                FailurePayloads::from(DecodablePayloads::new(
371                    details.payloads,
372                    payload_converter.clone(),
373                    *context,
374                ))
375            }),
376            failure: Some(failure),
377            cause: cause.map(Box::new),
378        }
379    }
380}
381
382impl std::fmt::Display for ApplicationFailure {
383    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384        write!(f, "{}", self.source)
385    }
386}
387
388impl std::error::Error for ApplicationFailure {
389    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
390        self.cause
391            .as_deref()
392            .map(|cause| cause as &(dyn std::error::Error + 'static))
393            .or_else(|| Some(self.source.as_ref()))
394    }
395}
396
397impl From<anyhow::Error> for ApplicationFailure {
398    fn from(value: anyhow::Error) -> Self {
399        Self::new(value)
400    }
401}
402
403impl From<PayloadConversionError> for ApplicationFailure {
404    fn from(value: PayloadConversionError) -> Self {
405        Self::new(value)
406    }
407}
408
409/// A typed outbound error surface used before encoding to a Temporal failure proto.
410#[derive(Debug, thiserror::Error)]
411pub enum OutgoingError {
412    /// An error produced while completing an activity.
413    #[error(transparent)]
414    Activity(#[from] OutgoingActivityError),
415    /// An error produced from a workflow.
416    #[error(transparent)]
417    Workflow(#[from] OutgoingWorkflowError),
418}
419
420/// A typed outbound activity error.
421#[derive(Debug, thiserror::Error)]
422pub enum OutgoingActivityError {
423    /// An activity application failure.
424    #[error(transparent)]
425    Application(#[from] Box<ApplicationFailure>),
426    /// An activity cancellation with optional details.
427    #[error("Activity cancelled")]
428    Cancelled {
429        /// Optional cancellation details.
430        details: Option<FailurePayloads>,
431    },
432}
433
434/// A typed outbound workflow failure.
435#[derive(Debug, thiserror::Error)]
436pub enum OutgoingWorkflowError {
437    /// A workflow application failure.
438    #[error(transparent)]
439    Application(#[from] Box<ApplicationFailure>),
440    /// A workflow failure sourced from an activity execution.
441    #[error(transparent)]
442    ActivityExecution(#[from] Box<ActivityExecutionError>),
443    /// A workflow failure sourced from a child-workflow execution.
444    #[error(transparent)]
445    ChildWorkflowExecution(#[from] Box<ChildWorkflowExecutionError>),
446    /// A workflow failure sourced from child-workflow start.
447    #[error(transparent)]
448    ChildWorkflowStart(#[from] Box<ChildWorkflowStartError>),
449    /// A workflow failure sourced from signaling a workflow.
450    #[error(transparent)]
451    WorkflowSignal(#[from] Box<WorkflowSignalError>),
452}
453
454impl From<anyhow::Error> for OutgoingWorkflowError {
455    fn from(value: anyhow::Error) -> Self {
456        Self::Application(Box::new(ApplicationFailure::new(value)))
457    }
458}
459
460impl From<PayloadConversionError> for OutgoingWorkflowError {
461    fn from(value: PayloadConversionError) -> Self {
462        Self::Application(Box::new(value.into()))
463    }
464}
465
466impl From<ApplicationFailure> for OutgoingWorkflowError {
467    fn from(value: ApplicationFailure) -> Self {
468        Self::Application(Box::new(value))
469    }
470}
471
472impl From<ActivityExecutionError> for OutgoingWorkflowError {
473    fn from(value: ActivityExecutionError) -> Self {
474        Self::ActivityExecution(Box::new(value))
475    }
476}
477
478impl From<ChildWorkflowExecutionError> for OutgoingWorkflowError {
479    fn from(value: ChildWorkflowExecutionError) -> Self {
480        Self::ChildWorkflowExecution(Box::new(value))
481    }
482}
483
484impl From<ChildWorkflowStartError> for OutgoingWorkflowError {
485    fn from(value: ChildWorkflowStartError) -> Self {
486        Self::ChildWorkflowStart(Box::new(value))
487    }
488}
489
490impl From<WorkflowSignalError> for OutgoingWorkflowError {
491    fn from(value: WorkflowSignalError) -> Self {
492        Self::WorkflowSignal(Box::new(value))
493    }
494}
495
496/// A normalized incoming Temporal failure decoded from a protobuf [`Failure`].
497#[derive(Debug)]
498pub enum IncomingError {
499    /// A decoded application failure.
500    Application(ApplicationFailure),
501    /// A decoded timeout failure.
502    Timeout(TimeoutError),
503    /// A decoded cancellation failure.
504    Cancelled(CancelledError),
505    /// A decoded terminated failure.
506    Terminated(TerminatedError),
507    /// A decoded server failure.
508    Server(ServerError),
509    /// A decoded reset-workflow failure.
510    ResetWorkflow(ResetWorkflowError),
511    /// A decoded activity failure wrapper.
512    Activity(ActivityFailureError),
513    /// A decoded child-workflow failure wrapper.
514    ChildWorkflowExecution(ChildWorkflowFailureError),
515    /// A decoded nexus operation failure wrapper.
516    NexusOperationExecution(IncomingNexusOperationExecutionError),
517    /// A decoded nexus handler failure wrapper.
518    NexusHandler(IncomingNexusHandlerError),
519}
520
521impl IncomingError {
522    /// Returns the original failure proto for this normalized error.
523    pub fn failure(&self) -> &Failure {
524        match self {
525            IncomingError::Application(err) => err
526                .failure()
527                .expect("decoded application failures retain their original proto"),
528            IncomingError::Timeout(err) => err.failure(),
529            IncomingError::Cancelled(err) => err.failure(),
530            IncomingError::Terminated(err) => err.failure(),
531            IncomingError::Server(err) => err.failure(),
532            IncomingError::ResetWorkflow(err) => err.failure(),
533            IncomingError::Activity(err) => err.failure(),
534            IncomingError::ChildWorkflowExecution(err) => err.failure(),
535            IncomingError::NexusOperationExecution(err) => err.failure(),
536            IncomingError::NexusHandler(err) => err.failure(),
537        }
538    }
539
540    /// Returns the normalized cause, if any.
541    pub fn cause(&self) -> Option<&IncomingError> {
542        match self {
543            IncomingError::Application(err) => err.cause(),
544            IncomingError::Timeout(err) => err.cause(),
545            IncomingError::Cancelled(err) => err.cause(),
546            IncomingError::Terminated(err) => err.cause(),
547            IncomingError::Server(err) => err.cause(),
548            IncomingError::ResetWorkflow(err) => err.cause(),
549            IncomingError::Activity(err) => err.cause(),
550            IncomingError::ChildWorkflowExecution(err) => err.cause(),
551            IncomingError::NexusOperationExecution(err) => err.cause(),
552            IncomingError::NexusHandler(err) => err.cause(),
553        }
554    }
555
556    /// Consumes this normalized error and returns the retained proto failure.
557    pub fn into_failure(self) -> Failure {
558        match self {
559            IncomingError::Application(err) => err
560                .into_failure()
561                .expect("decoded application failures retain their original proto"),
562            IncomingError::Timeout(err) => err.into_failure(),
563            IncomingError::Cancelled(err) => err.into_failure(),
564            IncomingError::Terminated(err) => err.into_failure(),
565            IncomingError::Server(err) => err.into_failure(),
566            IncomingError::ResetWorkflow(err) => err.into_failure(),
567            IncomingError::Activity(err) => err.into_failure(),
568            IncomingError::ChildWorkflowExecution(err) => err.into_failure(),
569            IncomingError::NexusOperationExecution(err) => err.into_failure(),
570            IncomingError::NexusHandler(err) => err.into_failure(),
571        }
572    }
573
574    /// If the [`IncomingError`] is a timeout, returns the associated [`TimeoutError`].
575    pub fn as_timeout(&self) -> Option<&TimeoutError> {
576        match self {
577            IncomingError::Timeout(err) => Some(err),
578            _ => None,
579        }
580    }
581
582    /// If the [`IncomingError`] is a cancellation, returns the associated [`CancelledError`].
583    pub fn as_cancelled(&self) -> Option<&CancelledError> {
584        match self {
585            IncomingError::Cancelled(err) => Some(err),
586            _ => None,
587        }
588    }
589}
590
591impl std::fmt::Display for IncomingError {
592    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
593        match self {
594            IncomingError::Application(err) => err.fmt(f),
595            IncomingError::Timeout(err) => err.fmt(f),
596            IncomingError::Cancelled(err) => err.fmt(f),
597            IncomingError::Terminated(err) => err.fmt(f),
598            IncomingError::Server(err) => err.fmt(f),
599            IncomingError::ResetWorkflow(err) => err.fmt(f),
600            IncomingError::Activity(err) => err.fmt(f),
601            IncomingError::ChildWorkflowExecution(err) => err.fmt(f),
602            IncomingError::NexusOperationExecution(err) => err.fmt(f),
603            IncomingError::NexusHandler(err) => err.fmt(f),
604        }
605    }
606}
607
608impl std::error::Error for IncomingError {
609    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
610        match self {
611            IncomingError::Application(err) => Some(err),
612            IncomingError::Timeout(err) => Some(err),
613            IncomingError::Cancelled(err) => Some(err),
614            IncomingError::Terminated(err) => Some(err),
615            IncomingError::Server(err) => Some(err),
616            IncomingError::ResetWorkflow(err) => Some(err),
617            IncomingError::Activity(err) => Some(err),
618            IncomingError::ChildWorkflowExecution(err) => Some(err),
619            IncomingError::NexusOperationExecution(err) => Some(err),
620            IncomingError::NexusHandler(err) => Some(err),
621        }
622    }
623}
624
625macro_rules! impl_incoming_failure_wrapper {
626    ($name:ident) => {
627        impl $name {
628            /// Returns the original failure proto.
629            pub fn failure(&self) -> &Failure {
630                &self.failure
631            }
632
633            /// Returns the normalized cause, if any.
634            pub fn cause(&self) -> Option<&IncomingError> {
635                self.cause.as_deref()
636            }
637
638            /// Consumes this wrapper and returns the retained proto failure.
639            pub fn into_failure(self) -> Failure {
640                self.failure
641            }
642
643            /// Consumes this wrapper and returns the retained proto failure and normalized cause.
644            pub fn into_parts(self) -> (Failure, Option<IncomingError>) {
645                (self.failure, self.cause.map(|cause| *cause))
646            }
647        }
648
649        impl std::fmt::Display for $name {
650            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
651                self.failure.fmt(f)
652            }
653        }
654
655        impl std::error::Error for $name {
656            fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
657                self.cause
658                    .as_deref()
659                    .map(|cause| cause as &(dyn std::error::Error + 'static))
660            }
661        }
662    };
663}
664
665macro_rules! incoming_failure_wrapper {
666    ($name:ident, $doc:literal) => {
667        #[doc = $doc]
668        #[derive(Debug)]
669        pub struct $name {
670            failure: Failure,
671            cause: Option<Box<IncomingError>>,
672        }
673
674        impl $name {
675            /// Creates a new normalized incoming error wrapper.
676            pub(crate) fn new(failure: Failure, cause: Option<IncomingError>) -> Self {
677                Self {
678                    failure,
679                    cause: cause.map(Box::new),
680                }
681            }
682        }
683
684        impl_incoming_failure_wrapper!($name);
685    };
686}
687
688/// A normalized timeout failure.
689#[derive(Debug)]
690pub struct TimeoutError {
691    failure: Failure,
692    cause: Option<Box<IncomingError>>,
693    timeout_type: TimeoutType,
694    last_heartbeat_details: Option<DecodablePayloads>,
695}
696
697impl TimeoutError {
698    /// Creates a new normalized timeout error wrapper.
699    pub(crate) fn new(
700        failure: Failure,
701        failure_info: crate::protos::temporal::api::failure::v1::TimeoutFailureInfo,
702        cause: Option<IncomingError>,
703        payload_converter: &PayloadConverter,
704        context: &SerializationContextData,
705    ) -> Self {
706        Self {
707            failure,
708            cause: cause.map(Box::new),
709            timeout_type: TimeoutType::from_raw(failure_info.timeout_type),
710            last_heartbeat_details: failure_info.last_heartbeat_details.map(|details| {
711                DecodablePayloads::new(details.payloads, payload_converter.clone(), *context)
712            }),
713        }
714    }
715
716    /// Returns the timeout kind described by the failure.
717    pub fn timeout_type(&self) -> TimeoutType {
718        self.timeout_type
719    }
720
721    /// Returns the last heartbeat details carried by the timeout, if any.
722    pub fn last_heartbeat_details<T: TemporalDeserializable + 'static>(
723        &self,
724    ) -> Result<Option<T>, PayloadConversionError> {
725        self.last_heartbeat_details
726            .as_ref()
727            .map(DecodablePayloads::deserialize)
728            .transpose()
729    }
730
731    /// Returns the raw decoded heartbeat details carried by the timeout, if any.
732    pub fn raw_last_heartbeat_details(&self) -> Option<&[Payload]> {
733        self.last_heartbeat_details
734            .as_ref()
735            .map(DecodablePayloads::raw)
736    }
737}
738
739impl_incoming_failure_wrapper!(TimeoutError);
740
741/// A normalized cancellation failure.
742#[derive(Debug)]
743pub struct CancelledError {
744    failure: Failure,
745    cause: Option<Box<IncomingError>>,
746    details: Option<DecodablePayloads>,
747}
748
749impl CancelledError {
750    /// Creates a new normalized cancellation error wrapper.
751    pub(crate) fn new(
752        failure: Failure,
753        failure_info: crate::protos::temporal::api::failure::v1::CanceledFailureInfo,
754        cause: Option<IncomingError>,
755        payload_converter: &PayloadConverter,
756        context: &SerializationContextData,
757    ) -> Self {
758        Self {
759            failure,
760            cause: cause.map(Box::new),
761            details: failure_info.details.map(|details| {
762                DecodablePayloads::new(details.payloads, payload_converter.clone(), *context)
763            }),
764        }
765    }
766
767    /// Returns the cancellation details carried by the failure, deserialized as the requested
768    /// type, if any.
769    pub fn details<T: TemporalDeserializable + 'static>(
770        &self,
771    ) -> Result<Option<T>, PayloadConversionError> {
772        self.details
773            .as_ref()
774            .map(DecodablePayloads::deserialize)
775            .transpose()
776    }
777
778    /// Returns the raw decoded cancellation details carried by the failure, if any.
779    pub fn raw_details(&self) -> Option<&[Payload]> {
780        self.details.as_ref().map(DecodablePayloads::raw)
781    }
782}
783
784impl_incoming_failure_wrapper!(CancelledError);
785incoming_failure_wrapper!(TerminatedError, "A normalized terminated failure.");
786incoming_failure_wrapper!(ServerError, "A normalized server failure.");
787incoming_failure_wrapper!(ResetWorkflowError, "A normalized reset-workflow failure.");
788
789/// A normalized activity failure wrapper.
790#[derive(Debug)]
791pub struct ActivityFailureError {
792    failure: Failure,
793    cause: Option<Box<IncomingError>>,
794    activity_id: String,
795    activity_type: Option<String>,
796    scheduled_event_id: i64,
797    started_event_id: i64,
798    identity: String,
799    retry_state: RetryState,
800}
801
802impl ActivityFailureError {
803    /// Creates a new normalized activity failure wrapper.
804    pub(crate) fn new(
805        failure: Failure,
806        failure_info: crate::protos::temporal::api::failure::v1::ActivityFailureInfo,
807        cause: Option<IncomingError>,
808    ) -> Self {
809        let retry_state = RetryState::from_raw(failure_info.retry_state);
810        Self {
811            failure,
812            cause: cause.map(Box::new),
813            activity_id: failure_info.activity_id,
814            activity_type: failure_info
815                .activity_type
816                .map(|activity_type| activity_type.name),
817            scheduled_event_id: failure_info.scheduled_event_id,
818            started_event_id: failure_info.started_event_id,
819            identity: failure_info.identity,
820            retry_state,
821        }
822    }
823
824    /// Returns the activity id reported by the failure.
825    pub fn activity_id(&self) -> &str {
826        &self.activity_id
827    }
828
829    /// Returns the activity type, if present.
830    pub fn activity_type(&self) -> Option<&str> {
831        self.activity_type.as_deref()
832    }
833
834    /// Returns the scheduled event id.
835    pub fn scheduled_event_id(&self) -> i64 {
836        self.scheduled_event_id
837    }
838
839    /// Returns the started event id.
840    pub fn started_event_id(&self) -> i64 {
841        self.started_event_id
842    }
843
844    /// Returns the worker identity captured on the failure.
845    pub fn identity(&self) -> &str {
846        &self.identity
847    }
848
849    /// Returns the retry state reported by core.
850    pub fn retry_state(&self) -> RetryState {
851        self.retry_state
852    }
853
854    /// If this [`ActivityFailureError`] was caused by a timeout, returns the associated
855    /// [`TimeoutError`].
856    pub fn as_timeout(&self) -> Option<&TimeoutError> {
857        self.cause().and_then(IncomingError::as_timeout)
858    }
859
860    /// If this [`ActivityFailureError`] was caused by a cancellation, returns the associated
861    /// [`CancelledError`].
862    pub fn as_cancelled(&self) -> Option<&CancelledError> {
863        self.cause().and_then(IncomingError::as_cancelled)
864    }
865}
866
867impl_incoming_failure_wrapper!(ActivityFailureError);
868/// A normalized child-workflow execution failure wrapper.
869#[derive(Debug)]
870pub struct ChildWorkflowFailureError {
871    failure: Failure,
872    cause: Option<Box<IncomingError>>,
873    namespace: String,
874    workflow_execution: Option<WorkflowExecution>,
875    workflow_type: Option<String>,
876    initiated_event_id: i64,
877    started_event_id: i64,
878    retry_state: RetryState,
879}
880
881impl ChildWorkflowFailureError {
882    /// Creates a new normalized child-workflow execution failure wrapper.
883    pub(crate) fn new(
884        failure: Failure,
885        failure_info: crate::protos::temporal::api::failure::v1::ChildWorkflowExecutionFailureInfo,
886        cause: Option<IncomingError>,
887    ) -> Self {
888        let retry_state = RetryState::from_raw(failure_info.retry_state);
889        Self {
890            failure,
891            cause: cause.map(Box::new),
892            namespace: failure_info.namespace,
893            workflow_execution: failure_info.workflow_execution.map(Into::into),
894            workflow_type: failure_info
895                .workflow_type
896                .map(|workflow_type| workflow_type.name),
897            initiated_event_id: failure_info.initiated_event_id,
898            started_event_id: failure_info.started_event_id,
899            retry_state,
900        }
901    }
902
903    /// Returns the namespace of the child workflow.
904    pub fn namespace(&self) -> &str {
905        &self.namespace
906    }
907
908    /// Returns the child workflow execution, if present.
909    pub fn workflow_execution(&self) -> Option<&WorkflowExecution> {
910        self.workflow_execution.as_ref()
911    }
912
913    /// Returns the child workflow type, if present.
914    pub fn workflow_type(&self) -> Option<&str> {
915        self.workflow_type.as_deref()
916    }
917
918    /// Returns the initiated event id.
919    pub fn initiated_event_id(&self) -> i64 {
920        self.initiated_event_id
921    }
922
923    /// Returns the started event id.
924    pub fn started_event_id(&self) -> i64 {
925        self.started_event_id
926    }
927
928    /// Returns the retry state reported by core.
929    pub fn retry_state(&self) -> RetryState {
930        self.retry_state
931    }
932
933    /// If this [`ChildWorkflowFailureError`] was caused by a timeout, returns the associated
934    /// [`TimeoutError`].
935    pub fn as_timeout(&self) -> Option<&TimeoutError> {
936        self.cause().and_then(IncomingError::as_timeout)
937    }
938
939    /// If this [`ChildWorkflowFailureError`] was caused by a cancellation, returns the associated
940    /// [`CancelledError`].
941    pub fn as_cancelled(&self) -> Option<&CancelledError> {
942        self.cause().and_then(IncomingError::as_cancelled)
943    }
944}
945
946impl_incoming_failure_wrapper!(ChildWorkflowFailureError);
947incoming_failure_wrapper!(
948    IncomingNexusOperationExecutionError,
949    "A normalized nexus operation failure wrapper."
950);
951incoming_failure_wrapper!(
952    IncomingNexusHandlerError,
953    "A normalized nexus handler failure wrapper."
954);
955
956/// Error type for activity execution outcomes.
957#[derive(Debug, thiserror::Error)]
958pub enum ActivityExecutionError {
959    /// The activity failed with the given failure details.
960    #[error("Activity failed: {}", .0.failure().message)]
961    Failed(#[source] ActivityFailureError),
962    /// The activity was cancelled.
963    #[error("Activity cancelled: {}", .0.failure().message)]
964    Cancelled(#[source] CancelledError),
965    /// Failed to serialize input or deserialize result payload.
966    #[error("Payload conversion failed: {0}")]
967    Serialization(#[from] PayloadConversionError),
968}
969
970impl ActivityExecutionError {
971    /// Returns the retained top-level activity failure proto, if one exists.
972    pub fn failure(&self) -> Option<&Failure> {
973        match self {
974            ActivityExecutionError::Failed(err) => Some(err.failure()),
975            ActivityExecutionError::Cancelled(err) => Some(err.failure()),
976            ActivityExecutionError::Serialization(_) => None,
977        }
978    }
979
980    /// Returns the normalized cause of the top-level activity failure, if any.
981    pub fn cause(&self) -> Option<&IncomingError> {
982        match self {
983            ActivityExecutionError::Failed(err) => err.cause(),
984            ActivityExecutionError::Cancelled(err) => err.cause(),
985            ActivityExecutionError::Serialization(_) => None,
986        }
987    }
988
989    /// Returns the underlying failure reason for wrapper-shaped activity failures.
990    pub fn reason(&self) -> Option<&IncomingError> {
991        match self {
992            ActivityExecutionError::Failed(err) => err.cause(),
993            ActivityExecutionError::Cancelled(_) | ActivityExecutionError::Serialization(_) => None,
994        }
995    }
996
997    /// If this [`ActivityExecutionError`] was caused by a timeout, returns the associated
998    /// [`TimeoutError`].
999    pub fn as_timeout(&self) -> Option<&TimeoutError> {
1000        match self {
1001            ActivityExecutionError::Failed(err) => err.as_timeout(),
1002            ActivityExecutionError::Serialization(_) | ActivityExecutionError::Cancelled(_) => None,
1003        }
1004    }
1005
1006    /// If this [`ActivityExecutionError`] was caused by a cancellation, returns the associated
1007    /// [`CancelledError`].
1008    pub fn as_cancelled(&self) -> Option<&CancelledError> {
1009        match self {
1010            ActivityExecutionError::Failed(err) => err.as_cancelled(),
1011            ActivityExecutionError::Cancelled(err) => Some(err),
1012            ActivityExecutionError::Serialization(_) => None,
1013        }
1014    }
1015}
1016
1017/// Error returned when starting a child workflow fails.
1018#[derive(Debug, thiserror::Error)]
1019pub enum ChildWorkflowStartError {
1020    /// The child workflow start was cancelled before the normal execution wrapper path existed.
1021    #[error("Child workflow start cancelled: {}", .0.failure().message)]
1022    Cancelled(#[source] Box<CancelledError>),
1023    /// The child workflow failed to start (e.g., workflow ID already exists).
1024    #[error(
1025        "Child workflow start failed: workflow_id={workflow_id}, workflow_type={workflow_type}, cause={cause:?}"
1026    )]
1027    StartFailed {
1028        /// The workflow ID that was requested.
1029        workflow_id: String,
1030        /// The workflow type that was requested.
1031        workflow_type: String,
1032        /// The cause of the start failure.
1033        cause: StartChildWorkflowExecutionFailedCause,
1034    },
1035    /// Failed to serialize child workflow input payloads.
1036    #[error("Payload conversion failed: {0}")]
1037    Serialization(#[from] PayloadConversionError),
1038}
1039
1040impl ChildWorkflowStartError {
1041    /// Returns the retained top-level failure proto, if one exists.
1042    pub fn failure(&self) -> Option<&Failure> {
1043        match self {
1044            ChildWorkflowStartError::Cancelled(err) => Some(err.failure()),
1045            ChildWorkflowStartError::StartFailed { .. }
1046            | ChildWorkflowStartError::Serialization(_) => None,
1047        }
1048    }
1049
1050    /// Returns the normalized cause of the retained failure proto, if any.
1051    pub fn cause(&self) -> Option<&IncomingError> {
1052        match self {
1053            ChildWorkflowStartError::Cancelled(err) => err.cause(),
1054            ChildWorkflowStartError::StartFailed { .. }
1055            | ChildWorkflowStartError::Serialization(_) => None,
1056        }
1057    }
1058}
1059
1060/// Error returned when a child workflow execution fails.
1061#[derive(Debug, thiserror::Error)]
1062pub enum ChildWorkflowExecutionError {
1063    /// The child workflow failed.
1064    #[error("Child workflow failed: {}", .0.failure().message)]
1065    Failed(#[source] Box<ChildWorkflowFailureError>),
1066    /// Failed to serialize input or deserialize the child workflow result payload.
1067    #[error("Payload conversion failed: {0}")]
1068    Serialization(#[from] PayloadConversionError),
1069}
1070
1071impl ChildWorkflowExecutionError {
1072    /// Returns the retained top-level child-workflow failure proto, if one exists.
1073    pub fn failure(&self) -> Option<&Failure> {
1074        match self {
1075            ChildWorkflowExecutionError::Failed(err) => Some(err.failure()),
1076            ChildWorkflowExecutionError::Serialization(_) => None,
1077        }
1078    }
1079
1080    /// Returns the normalized cause of the top-level child-workflow failure, if any.
1081    pub fn cause(&self) -> Option<&IncomingError> {
1082        match self {
1083            ChildWorkflowExecutionError::Failed(err) => err.cause(),
1084            ChildWorkflowExecutionError::Serialization(_) => None,
1085        }
1086    }
1087
1088    /// Returns the underlying failure reason for wrapper-shaped child-workflow failures.
1089    pub fn reason(&self) -> Option<&IncomingError> {
1090        match self {
1091            ChildWorkflowExecutionError::Failed(err) => err.cause(),
1092            ChildWorkflowExecutionError::Serialization(_) => None,
1093        }
1094    }
1095
1096    /// If this [`ChildWorkflowExecutionError`] was caused by a timeout, returns the associated
1097    /// [`TimeoutError`].
1098    pub fn as_timeout(&self) -> Option<&TimeoutError> {
1099        match self {
1100            ChildWorkflowExecutionError::Failed(err) => err.as_timeout(),
1101            ChildWorkflowExecutionError::Serialization(_) => None,
1102        }
1103    }
1104
1105    /// If this [`ChildWorkflowExecutionError`] was caused by a cancellation, returns the associated
1106    /// [`CancelledError`].
1107    pub fn as_cancelled(&self) -> Option<&CancelledError> {
1108        match self {
1109            ChildWorkflowExecutionError::Failed(err) => err.as_cancelled(),
1110            ChildWorkflowExecutionError::Serialization(_) => None,
1111        }
1112    }
1113}
1114
1115/// Error returned when signaling a workflow fails.
1116#[derive(Debug, thiserror::Error)]
1117pub enum WorkflowSignalError {
1118    /// The signal delivery failed.
1119    #[error("Child workflow signal failed: {}", .0.failure().message)]
1120    Failed(#[source] Box<WorkflowSignalFailureError>),
1121    /// Failed to serialize the signal input payload.
1122    #[error("Signal payload conversion failed: {0}")]
1123    Serialization(#[from] PayloadConversionError),
1124}
1125
1126impl WorkflowSignalError {
1127    /// Returns the retained top-level workflow signal failure proto, if one exists.
1128    pub fn failure(&self) -> Option<&Failure> {
1129        match self {
1130            WorkflowSignalError::Failed(err) => Some(err.failure()),
1131            WorkflowSignalError::Serialization(_) => None,
1132        }
1133    }
1134
1135    /// Returns the normalized cause of the workflow signal failure, if any.
1136    pub fn cause(&self) -> Option<&IncomingError> {
1137        match self {
1138            WorkflowSignalError::Failed(err) => err.cause(),
1139            WorkflowSignalError::Serialization(_) => None,
1140        }
1141    }
1142
1143    /// Returns the underlying failure reason for wrapper-shaped signal failures.
1144    pub fn reason(&self) -> Option<&IncomingError> {
1145        match self {
1146            WorkflowSignalError::Failed(err) => Some(err.error()),
1147            WorkflowSignalError::Serialization(_) => None,
1148        }
1149    }
1150}
1151
1152/// A normalized workflow signal failure wrapper.
1153#[derive(Debug)]
1154pub struct WorkflowSignalFailureError {
1155    failure: Failure,
1156    error: Box<IncomingError>,
1157}
1158
1159impl WorkflowSignalFailureError {
1160    /// Creates a workflow signal failure wrapper.
1161    pub(crate) fn new(failure: Failure, error: IncomingError) -> Self {
1162        Self {
1163            failure,
1164            error: Box::new(error),
1165        }
1166    }
1167
1168    /// Returns the retained top-level proto failure.
1169    pub fn failure(&self) -> &Failure {
1170        &self.failure
1171    }
1172
1173    /// Returns the normalized direct cause of the workflow signal failure, if any.
1174    pub fn cause(&self) -> Option<&IncomingError> {
1175        self.error.cause()
1176    }
1177
1178    /// Returns the direct decoded incoming error represented by the top-level proto failure.
1179    pub fn error(&self) -> &IncomingError {
1180        &self.error
1181    }
1182}
1183
1184impl std::fmt::Display for WorkflowSignalFailureError {
1185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1186        self.failure.fmt(f)
1187    }
1188}
1189
1190impl std::error::Error for WorkflowSignalFailureError {
1191    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1192        self.cause()
1193            .map(|cause| cause as &(dyn std::error::Error + 'static))
1194    }
1195}
1196
1197#[cfg(test)]
1198mod tests {
1199    use super::*;
1200    use crate::{
1201        data_converters::{
1202            DefaultFailureConverter, FailureConverter, GenericPayloadConverter, PayloadConverter,
1203            SerializationContext, SerializationContextData,
1204        },
1205        protos::temporal::api::{
1206            common::v1::Payload,
1207            failure::v1::{ActivityFailureInfo, TimeoutFailureInfo, failure::FailureInfo},
1208        },
1209    };
1210
1211    struct AlwaysFailsSerialize;
1212
1213    impl serde::Serialize for AlwaysFailsSerialize {
1214        fn serialize<S: serde::Serializer>(&self, _serializer: S) -> Result<S::Ok, S::Error> {
1215            Err(serde::ser::Error::custom("serialize boom"))
1216        }
1217    }
1218
1219    #[test]
1220    fn decoded_failures_hide_unknown_state_values() {
1221        let failure = Failure {
1222            cause: Some(Box::new(Failure {
1223                failure_info: Some(FailureInfo::TimeoutFailureInfo(TimeoutFailureInfo {
1224                    timeout_type: 654_321,
1225                    ..Default::default()
1226                })),
1227                ..Default::default()
1228            })),
1229            failure_info: Some(FailureInfo::ActivityFailureInfo(ActivityFailureInfo {
1230                retry_state: 123_456,
1231                ..Default::default()
1232            })),
1233            ..Default::default()
1234        };
1235
1236        let decoded = DefaultFailureConverter
1237            .to_error(
1238                failure,
1239                &PayloadConverter::default(),
1240                &SerializationContextData::Workflow,
1241            )
1242            .unwrap();
1243        let IncomingError::Activity(activity) = decoded else {
1244            panic!("expected activity failure");
1245        };
1246        assert_eq!(activity.retry_state(), RetryState::Unknown);
1247        let Some(FailureInfo::ActivityFailureInfo(raw_activity)) =
1248            activity.failure().failure_info.as_ref()
1249        else {
1250            panic!("expected raw activity failure info");
1251        };
1252        assert_eq!(raw_activity.retry_state, 123_456);
1253        let Some(IncomingError::Timeout(timeout)) = activity.cause() else {
1254            panic!("expected timeout cause");
1255        };
1256        assert_eq!(timeout.timeout_type(), TimeoutType::Unknown);
1257        let Some(FailureInfo::TimeoutFailureInfo(raw_timeout)) =
1258            timeout.failure().failure_info.as_ref()
1259        else {
1260            panic!("expected raw timeout failure info");
1261        };
1262        assert_eq!(raw_timeout.timeout_type, 654_321);
1263    }
1264
1265    #[test]
1266    fn constructors_set_retryability_defaults() {
1267        assert!(!ApplicationFailure::new(anyhow::anyhow!("retryable")).is_non_retryable());
1268        assert!(
1269            ApplicationFailure::non_retryable(anyhow::anyhow!("non-retryable")).is_non_retryable()
1270        );
1271    }
1272
1273    #[test]
1274    fn conversion_preserves_application_metadata() {
1275        let payloads = Payloads {
1276            payloads: vec![Payload {
1277                data: b"details".to_vec(),
1278                ..Default::default()
1279            }],
1280        };
1281        let failure = DefaultFailureConverter.to_failure(
1282            OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
1283                ApplicationFailure::builder(anyhow::anyhow!("oops"))
1284                    .type_name("MyType".to_owned())
1285                    .non_retryable(true)
1286                    .next_retry_delay(Duration::from_secs(3))
1287                    .category(ApplicationErrorCategory::Benign)
1288                    .details(RawValue::new(payloads.payloads.clone()))
1289                    .build(),
1290            ))),
1291            &PayloadConverter::default(),
1292            &SerializationContextData::None,
1293        );
1294        let Some(FailureInfo::ApplicationFailureInfo(info)) = failure.failure_info else {
1295            panic!("expected application failure info");
1296        };
1297        assert_eq!(failure.message, "oops");
1298        assert_eq!(info.r#type, "MyType");
1299        assert!(info.non_retryable);
1300        assert_eq!(info.details, Some(payloads));
1301        assert_eq!(info.category(), ProtoApplicationErrorCategory::Benign);
1302        assert_eq!(info.next_retry_delay.unwrap().seconds, 3);
1303    }
1304
1305    #[test]
1306    fn builder_accepts_raw_payload_details() {
1307        let payload = Payload {
1308            data: b"details".to_vec(),
1309            ..Default::default()
1310        };
1311        let failure = DefaultFailureConverter.to_failure(
1312            OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
1313                ApplicationFailure::builder(anyhow::anyhow!("oops"))
1314                    .details(RawValue::new(vec![payload.clone()]))
1315                    .build(),
1316            ))),
1317            &PayloadConverter::default(),
1318            &SerializationContextData::None,
1319        );
1320
1321        let Some(FailureInfo::ApplicationFailureInfo(info)) = failure.failure_info else {
1322            panic!("expected application failure info");
1323        };
1324        assert_eq!(info.details.unwrap().payloads, vec![payload]);
1325    }
1326
1327    #[test]
1328    fn builder_accepts_serializable_details() {
1329        let failure = DefaultFailureConverter.to_failure(
1330            OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
1331                ApplicationFailure::builder(anyhow::anyhow!("oops"))
1332                    .details("details".to_string())
1333                    .build(),
1334            ))),
1335            &PayloadConverter::default(),
1336            &SerializationContextData::None,
1337        );
1338
1339        let Some(FailureInfo::ApplicationFailureInfo(info)) = failure.failure_info else {
1340            panic!("expected application failure info");
1341        };
1342        let payloads = info.details.expect("expected details").payloads;
1343        let converter = PayloadConverter::default();
1344        let details: String = converter
1345            .from_payloads(
1346                &SerializationContext {
1347                    data: &SerializationContextData::None,
1348                    converter: &converter,
1349                },
1350                payloads,
1351            )
1352            .unwrap();
1353        assert_eq!(details, "details");
1354    }
1355
1356    #[test]
1357    fn application_failure_encoding_surfaces_detail_encoding_errors() {
1358        let failure = DefaultFailureConverter.to_failure(
1359            OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
1360                ApplicationFailure::builder(anyhow::anyhow!("oops"))
1361                    .details(AlwaysFailsSerialize)
1362                    .build(),
1363            ))),
1364            &PayloadConverter::default(),
1365            &SerializationContextData::None,
1366        );
1367
1368        assert_eq!(
1369            failure.message,
1370            "Failed converting error to failure: Encoding error: serialize boom, original error message: oops"
1371        );
1372        assert!(matches!(
1373            failure.failure_info,
1374            Some(FailureInfo::ApplicationFailureInfo(_))
1375        ));
1376    }
1377
1378    #[test]
1379    fn anyhow_workflow_errors_default_to_application_outgoing_errors() {
1380        let outgoing: OutgoingWorkflowError = anyhow::anyhow!("workflow boom").into();
1381
1382        let OutgoingWorkflowError::Application(app) = outgoing else {
1383            panic!("plain workflow errors should default to application failures");
1384        };
1385        assert_eq!(app.to_string(), "workflow boom");
1386    }
1387
1388    #[test]
1389    fn payload_conversion_errors_default_to_application_outgoing_errors() {
1390        let outgoing: OutgoingWorkflowError =
1391            PayloadConversionError::EncodingError(anyhow::anyhow!("encode boom").into()).into();
1392
1393        let OutgoingWorkflowError::Application(app) = outgoing else {
1394            panic!("payload conversion errors should default to application failures");
1395        };
1396        assert_eq!(app.to_string(), "Encoding error: encode boom");
1397    }
1398}