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