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)]
411#[non_exhaustive]
412pub enum OutgoingError {
413    /// An error produced while completing an activity.
414    #[error(transparent)]
415    Activity(#[from] OutgoingActivityError),
416    /// An error produced from a workflow.
417    #[error(transparent)]
418    Workflow(#[from] OutgoingWorkflowError),
419}
420
421/// A typed outbound activity error.
422#[derive(Debug, thiserror::Error)]
423#[non_exhaustive]
424pub enum OutgoingActivityError {
425    /// An activity application failure.
426    #[error(transparent)]
427    Application(#[from] Box<ApplicationFailure>),
428    /// An activity cancellation with optional details.
429    #[error("Activity cancelled")]
430    Cancelled {
431        /// Optional cancellation details.
432        details: Option<FailurePayloads>,
433    },
434}
435
436/// A typed outbound workflow failure.
437#[derive(Debug, thiserror::Error)]
438#[non_exhaustive]
439pub enum OutgoingWorkflowError {
440    /// A workflow application failure.
441    #[error(transparent)]
442    Application(#[from] Box<ApplicationFailure>),
443    /// A workflow payload conversion failure.
444    #[error(transparent)]
445    PayloadConversion(#[from] PayloadConversionError),
446    /// A workflow failure sourced from an activity execution.
447    #[error(transparent)]
448    ActivityExecution(#[from] Box<ActivityExecutionError>),
449    /// A workflow failure sourced from a child-workflow execution.
450    #[error(transparent)]
451    ChildWorkflowExecution(#[from] Box<ChildWorkflowExecutionError>),
452    /// A workflow failure sourced from child-workflow start.
453    #[error(transparent)]
454    ChildWorkflowStart(#[from] Box<ChildWorkflowStartError>),
455    /// A workflow failure sourced from signaling a workflow.
456    #[error(transparent)]
457    WorkflowSignal(#[from] Box<WorkflowSignalError>),
458}
459
460impl OutgoingWorkflowError {
461    /// If this workflow error was caused by cancellation, returns the associated
462    /// [`CancelledError`].
463    pub fn as_cancelled(&self) -> Option<&CancelledError> {
464        match self {
465            Self::Application(err) => err.as_cancelled(),
466            Self::PayloadConversion(_) => None,
467            Self::ActivityExecution(err) => err.as_cancelled(),
468            Self::ChildWorkflowExecution(err) => err.as_cancelled(),
469            Self::ChildWorkflowStart(err) => err.as_cancelled(),
470            Self::WorkflowSignal(err) => err.as_cancelled(),
471        }
472    }
473}
474
475impl From<anyhow::Error> for OutgoingWorkflowError {
476    fn from(value: anyhow::Error) -> Self {
477        Self::Application(Box::new(ApplicationFailure::new(value)))
478    }
479}
480
481impl From<ApplicationFailure> for OutgoingWorkflowError {
482    fn from(value: ApplicationFailure) -> Self {
483        Self::Application(Box::new(value))
484    }
485}
486
487impl From<ActivityExecutionError> for OutgoingWorkflowError {
488    fn from(value: ActivityExecutionError) -> Self {
489        match value {
490            ActivityExecutionError::Serialization(err) => Self::PayloadConversion(err),
491            other => Self::ActivityExecution(Box::new(other)),
492        }
493    }
494}
495
496impl From<ChildWorkflowExecutionError> for OutgoingWorkflowError {
497    fn from(value: ChildWorkflowExecutionError) -> Self {
498        match value {
499            ChildWorkflowExecutionError::Serialization(err) => Self::PayloadConversion(err),
500            other => Self::ChildWorkflowExecution(Box::new(other)),
501        }
502    }
503}
504
505impl From<ChildWorkflowStartError> for OutgoingWorkflowError {
506    fn from(value: ChildWorkflowStartError) -> Self {
507        match value {
508            ChildWorkflowStartError::Serialization(err) => Self::PayloadConversion(err),
509            other => Self::ChildWorkflowStart(Box::new(other)),
510        }
511    }
512}
513
514impl From<WorkflowSignalError> for OutgoingWorkflowError {
515    fn from(value: WorkflowSignalError) -> Self {
516        match value {
517            WorkflowSignalError::Serialization(err) => Self::PayloadConversion(err),
518            other => Self::WorkflowSignal(Box::new(other)),
519        }
520    }
521}
522
523/// A normalized incoming Temporal failure decoded from a protobuf [`Failure`].
524#[derive(Debug)]
525pub enum IncomingError {
526    /// A decoded application failure.
527    Application(ApplicationFailure),
528    /// A decoded timeout failure.
529    Timeout(TimeoutError),
530    /// A decoded cancellation failure.
531    Cancelled(CancelledError),
532    /// A decoded terminated failure.
533    Terminated(TerminatedError),
534    /// A decoded server failure.
535    Server(ServerError),
536    /// A decoded reset-workflow failure.
537    ResetWorkflow(ResetWorkflowError),
538    /// A decoded activity failure wrapper.
539    Activity(ActivityFailureError),
540    /// A decoded child-workflow failure wrapper.
541    ChildWorkflowExecution(ChildWorkflowFailureError),
542    /// A decoded nexus operation failure wrapper.
543    NexusOperationExecution(IncomingNexusOperationExecutionError),
544    /// A decoded nexus handler failure wrapper.
545    NexusHandler(IncomingNexusHandlerError),
546}
547
548impl IncomingError {
549    /// Returns the original failure proto for this normalized error.
550    pub fn failure(&self) -> &Failure {
551        match self {
552            IncomingError::Application(err) => err
553                .failure()
554                .expect("decoded application failures retain their original proto"),
555            IncomingError::Timeout(err) => err.failure(),
556            IncomingError::Cancelled(err) => err.failure(),
557            IncomingError::Terminated(err) => err.failure(),
558            IncomingError::Server(err) => err.failure(),
559            IncomingError::ResetWorkflow(err) => err.failure(),
560            IncomingError::Activity(err) => err.failure(),
561            IncomingError::ChildWorkflowExecution(err) => err.failure(),
562            IncomingError::NexusOperationExecution(err) => err.failure(),
563            IncomingError::NexusHandler(err) => err.failure(),
564        }
565    }
566
567    /// Returns the normalized cause, if any.
568    pub fn cause(&self) -> Option<&IncomingError> {
569        match self {
570            IncomingError::Application(err) => err.cause(),
571            IncomingError::Timeout(err) => err.cause(),
572            IncomingError::Cancelled(err) => err.cause(),
573            IncomingError::Terminated(err) => err.cause(),
574            IncomingError::Server(err) => err.cause(),
575            IncomingError::ResetWorkflow(err) => err.cause(),
576            IncomingError::Activity(err) => err.cause(),
577            IncomingError::ChildWorkflowExecution(err) => err.cause(),
578            IncomingError::NexusOperationExecution(err) => err.cause(),
579            IncomingError::NexusHandler(err) => err.cause(),
580        }
581    }
582
583    /// Consumes this normalized error and returns the retained proto failure.
584    pub fn into_failure(self) -> Failure {
585        match self {
586            IncomingError::Application(err) => err
587                .into_failure()
588                .expect("decoded application failures retain their original proto"),
589            IncomingError::Timeout(err) => err.into_failure(),
590            IncomingError::Cancelled(err) => err.into_failure(),
591            IncomingError::Terminated(err) => err.into_failure(),
592            IncomingError::Server(err) => err.into_failure(),
593            IncomingError::ResetWorkflow(err) => err.into_failure(),
594            IncomingError::Activity(err) => err.into_failure(),
595            IncomingError::ChildWorkflowExecution(err) => err.into_failure(),
596            IncomingError::NexusOperationExecution(err) => err.into_failure(),
597            IncomingError::NexusHandler(err) => err.into_failure(),
598        }
599    }
600
601    /// If the [`IncomingError`] is a timeout, returns the associated [`TimeoutError`].
602    pub fn as_timeout(&self) -> Option<&TimeoutError> {
603        match self {
604            IncomingError::Timeout(err) => Some(err),
605            _ => None,
606        }
607    }
608
609    /// If the [`IncomingError`] is a cancellation, returns the associated [`CancelledError`].
610    pub fn as_cancelled(&self) -> Option<&CancelledError> {
611        match self {
612            IncomingError::Cancelled(err) => Some(err),
613            _ => None,
614        }
615    }
616}
617
618impl std::fmt::Display for IncomingError {
619    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
620        match self {
621            IncomingError::Application(err) => err.fmt(f),
622            IncomingError::Timeout(err) => err.fmt(f),
623            IncomingError::Cancelled(err) => err.fmt(f),
624            IncomingError::Terminated(err) => err.fmt(f),
625            IncomingError::Server(err) => err.fmt(f),
626            IncomingError::ResetWorkflow(err) => err.fmt(f),
627            IncomingError::Activity(err) => err.fmt(f),
628            IncomingError::ChildWorkflowExecution(err) => err.fmt(f),
629            IncomingError::NexusOperationExecution(err) => err.fmt(f),
630            IncomingError::NexusHandler(err) => err.fmt(f),
631        }
632    }
633}
634
635impl std::error::Error for IncomingError {
636    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
637        match self {
638            IncomingError::Application(err) => Some(err),
639            IncomingError::Timeout(err) => Some(err),
640            IncomingError::Cancelled(err) => Some(err),
641            IncomingError::Terminated(err) => Some(err),
642            IncomingError::Server(err) => Some(err),
643            IncomingError::ResetWorkflow(err) => Some(err),
644            IncomingError::Activity(err) => Some(err),
645            IncomingError::ChildWorkflowExecution(err) => Some(err),
646            IncomingError::NexusOperationExecution(err) => Some(err),
647            IncomingError::NexusHandler(err) => Some(err),
648        }
649    }
650}
651
652macro_rules! impl_incoming_failure_wrapper {
653    ($name:ident) => {
654        impl $name {
655            /// Returns the original failure proto.
656            pub fn failure(&self) -> &Failure {
657                &self.failure
658            }
659
660            /// Returns the normalized cause, if any.
661            pub fn cause(&self) -> Option<&IncomingError> {
662                self.cause.as_deref()
663            }
664
665            /// Consumes this wrapper and returns the retained proto failure.
666            pub fn into_failure(self) -> Failure {
667                self.failure
668            }
669
670            /// Consumes this wrapper and returns the retained proto failure and normalized cause.
671            pub fn into_parts(self) -> (Failure, Option<IncomingError>) {
672                (self.failure, self.cause.map(|cause| *cause))
673            }
674        }
675
676        impl std::fmt::Display for $name {
677            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
678                self.failure.fmt(f)
679            }
680        }
681
682        impl std::error::Error for $name {
683            fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
684                self.cause
685                    .as_deref()
686                    .map(|cause| cause as &(dyn std::error::Error + 'static))
687            }
688        }
689    };
690}
691
692macro_rules! incoming_failure_wrapper {
693    ($name:ident, $doc:literal) => {
694        #[doc = $doc]
695        #[derive(Debug)]
696        pub struct $name {
697            failure: Failure,
698            cause: Option<Box<IncomingError>>,
699        }
700
701        impl $name {
702            /// Creates a new normalized incoming error wrapper.
703            pub(crate) fn new(failure: Failure, cause: Option<IncomingError>) -> Self {
704                Self {
705                    failure,
706                    cause: cause.map(Box::new),
707                }
708            }
709        }
710
711        impl_incoming_failure_wrapper!($name);
712    };
713}
714
715/// A normalized timeout failure.
716#[derive(Debug)]
717pub struct TimeoutError {
718    failure: Failure,
719    cause: Option<Box<IncomingError>>,
720    timeout_type: TimeoutType,
721    last_heartbeat_details: Option<DecodablePayloads>,
722}
723
724impl TimeoutError {
725    /// Creates a new normalized timeout error wrapper.
726    pub(crate) fn new(
727        failure: Failure,
728        failure_info: crate::protos::temporal::api::failure::v1::TimeoutFailureInfo,
729        cause: Option<IncomingError>,
730        payload_converter: &PayloadConverter,
731        context: &SerializationContextData,
732    ) -> Self {
733        Self {
734            failure,
735            cause: cause.map(Box::new),
736            timeout_type: TimeoutType::from_raw(failure_info.timeout_type),
737            last_heartbeat_details: failure_info.last_heartbeat_details.map(|details| {
738                DecodablePayloads::new(details.payloads, payload_converter.clone(), *context)
739            }),
740        }
741    }
742
743    /// Returns the timeout kind described by the failure.
744    pub fn timeout_type(&self) -> TimeoutType {
745        self.timeout_type
746    }
747
748    /// Returns the last heartbeat details carried by the timeout, if any.
749    pub fn last_heartbeat_details<T: TemporalDeserializable + 'static>(
750        &self,
751    ) -> Result<Option<T>, PayloadConversionError> {
752        self.last_heartbeat_details
753            .as_ref()
754            .map(DecodablePayloads::deserialize)
755            .transpose()
756    }
757
758    /// Returns the raw decoded heartbeat details carried by the timeout, if any.
759    pub fn raw_last_heartbeat_details(&self) -> Option<&[Payload]> {
760        self.last_heartbeat_details
761            .as_ref()
762            .map(DecodablePayloads::raw)
763    }
764}
765
766impl_incoming_failure_wrapper!(TimeoutError);
767
768/// A normalized cancellation failure.
769#[derive(Debug)]
770pub struct CancelledError {
771    failure: Failure,
772    cause: Option<Box<IncomingError>>,
773    details: Option<DecodablePayloads>,
774}
775
776impl CancelledError {
777    /// Creates a new normalized cancellation error wrapper.
778    pub(crate) fn new(
779        failure: Failure,
780        failure_info: crate::protos::temporal::api::failure::v1::CanceledFailureInfo,
781        cause: Option<IncomingError>,
782        payload_converter: &PayloadConverter,
783        context: &SerializationContextData,
784    ) -> Self {
785        Self {
786            failure,
787            cause: cause.map(Box::new),
788            details: failure_info.details.map(|details| {
789                DecodablePayloads::new(details.payloads, payload_converter.clone(), *context)
790            }),
791        }
792    }
793
794    /// Returns the cancellation details carried by the failure, deserialized as the requested
795    /// type, if any.
796    pub fn details<T: TemporalDeserializable + 'static>(
797        &self,
798    ) -> Result<Option<T>, PayloadConversionError> {
799        self.details
800            .as_ref()
801            .map(DecodablePayloads::deserialize)
802            .transpose()
803    }
804
805    /// Returns the raw decoded cancellation details carried by the failure, if any.
806    pub fn raw_details(&self) -> Option<&[Payload]> {
807        self.details.as_ref().map(DecodablePayloads::raw)
808    }
809}
810
811impl_incoming_failure_wrapper!(CancelledError);
812incoming_failure_wrapper!(TerminatedError, "A normalized terminated failure.");
813incoming_failure_wrapper!(ServerError, "A normalized server failure.");
814incoming_failure_wrapper!(ResetWorkflowError, "A normalized reset-workflow failure.");
815
816/// A normalized activity failure wrapper.
817#[derive(Debug)]
818pub struct ActivityFailureError {
819    failure: Failure,
820    cause: Option<Box<IncomingError>>,
821    activity_id: String,
822    activity_type: Option<String>,
823    scheduled_event_id: i64,
824    started_event_id: i64,
825    identity: String,
826    retry_state: RetryState,
827}
828
829impl ActivityFailureError {
830    /// Creates a new normalized activity failure wrapper.
831    pub(crate) fn new(
832        failure: Failure,
833        failure_info: crate::protos::temporal::api::failure::v1::ActivityFailureInfo,
834        cause: Option<IncomingError>,
835    ) -> Self {
836        let retry_state = RetryState::from_raw(failure_info.retry_state);
837        Self {
838            failure,
839            cause: cause.map(Box::new),
840            activity_id: failure_info.activity_id,
841            activity_type: failure_info
842                .activity_type
843                .map(|activity_type| activity_type.name),
844            scheduled_event_id: failure_info.scheduled_event_id,
845            started_event_id: failure_info.started_event_id,
846            identity: failure_info.identity,
847            retry_state,
848        }
849    }
850
851    /// Returns the activity id reported by the failure.
852    pub fn activity_id(&self) -> &str {
853        &self.activity_id
854    }
855
856    /// Returns the activity type, if present.
857    pub fn activity_type(&self) -> Option<&str> {
858        self.activity_type.as_deref()
859    }
860
861    /// Returns the scheduled event id.
862    pub fn scheduled_event_id(&self) -> i64 {
863        self.scheduled_event_id
864    }
865
866    /// Returns the started event id.
867    pub fn started_event_id(&self) -> i64 {
868        self.started_event_id
869    }
870
871    /// Returns the worker identity captured on the failure.
872    pub fn identity(&self) -> &str {
873        &self.identity
874    }
875
876    /// Returns the retry state reported by core.
877    pub fn retry_state(&self) -> RetryState {
878        self.retry_state
879    }
880
881    /// If this [`ActivityFailureError`] was caused by a timeout, returns the associated
882    /// [`TimeoutError`].
883    pub fn as_timeout(&self) -> Option<&TimeoutError> {
884        self.cause().and_then(IncomingError::as_timeout)
885    }
886
887    /// If this [`ActivityFailureError`] was caused by a cancellation, returns the associated
888    /// [`CancelledError`].
889    pub fn as_cancelled(&self) -> Option<&CancelledError> {
890        self.cause().and_then(IncomingError::as_cancelled)
891    }
892}
893
894impl_incoming_failure_wrapper!(ActivityFailureError);
895/// A normalized child-workflow execution failure wrapper.
896#[derive(Debug)]
897pub struct ChildWorkflowFailureError {
898    failure: Failure,
899    cause: Option<Box<IncomingError>>,
900    namespace: String,
901    workflow_execution: Option<WorkflowExecution>,
902    workflow_type: Option<String>,
903    initiated_event_id: i64,
904    started_event_id: i64,
905    retry_state: RetryState,
906}
907
908impl ChildWorkflowFailureError {
909    /// Creates a new normalized child-workflow execution failure wrapper.
910    pub(crate) fn new(
911        failure: Failure,
912        failure_info: crate::protos::temporal::api::failure::v1::ChildWorkflowExecutionFailureInfo,
913        cause: Option<IncomingError>,
914    ) -> Self {
915        let retry_state = RetryState::from_raw(failure_info.retry_state);
916        Self {
917            failure,
918            cause: cause.map(Box::new),
919            namespace: failure_info.namespace,
920            workflow_execution: failure_info.workflow_execution.map(Into::into),
921            workflow_type: failure_info
922                .workflow_type
923                .map(|workflow_type| workflow_type.name),
924            initiated_event_id: failure_info.initiated_event_id,
925            started_event_id: failure_info.started_event_id,
926            retry_state,
927        }
928    }
929
930    /// Returns the namespace of the child workflow.
931    pub fn namespace(&self) -> &str {
932        &self.namespace
933    }
934
935    /// Returns the child workflow execution, if present.
936    pub fn workflow_execution(&self) -> Option<&WorkflowExecution> {
937        self.workflow_execution.as_ref()
938    }
939
940    /// Returns the child workflow type, if present.
941    pub fn workflow_type(&self) -> Option<&str> {
942        self.workflow_type.as_deref()
943    }
944
945    /// Returns the initiated event id.
946    pub fn initiated_event_id(&self) -> i64 {
947        self.initiated_event_id
948    }
949
950    /// Returns the started event id.
951    pub fn started_event_id(&self) -> i64 {
952        self.started_event_id
953    }
954
955    /// Returns the retry state reported by core.
956    pub fn retry_state(&self) -> RetryState {
957        self.retry_state
958    }
959
960    /// If this [`ChildWorkflowFailureError`] was caused by a timeout, returns the associated
961    /// [`TimeoutError`].
962    pub fn as_timeout(&self) -> Option<&TimeoutError> {
963        self.cause().and_then(IncomingError::as_timeout)
964    }
965
966    /// If this [`ChildWorkflowFailureError`] was caused by a cancellation, returns the associated
967    /// [`CancelledError`].
968    pub fn as_cancelled(&self) -> Option<&CancelledError> {
969        self.cause().and_then(IncomingError::as_cancelled)
970    }
971}
972
973impl_incoming_failure_wrapper!(ChildWorkflowFailureError);
974incoming_failure_wrapper!(
975    IncomingNexusOperationExecutionError,
976    "A normalized nexus operation failure wrapper."
977);
978incoming_failure_wrapper!(
979    IncomingNexusHandlerError,
980    "A normalized nexus handler failure wrapper."
981);
982
983/// Error type for activity execution outcomes.
984#[derive(Debug, thiserror::Error)]
985pub enum ActivityExecutionError {
986    /// The activity failed with the given failure details.
987    #[error("Activity failed: {}", .0.failure().message)]
988    Failed(#[source] ActivityFailureError),
989    /// The activity was cancelled.
990    #[error("Activity cancelled: {}", .0.failure().message)]
991    Cancelled(#[source] CancelledError),
992    /// Failed to serialize input or deserialize result payload.
993    #[error("Payload conversion failed: {0}")]
994    Serialization(#[from] PayloadConversionError),
995}
996
997impl ActivityExecutionError {
998    /// Returns the retained top-level activity failure proto, if one exists.
999    pub fn failure(&self) -> Option<&Failure> {
1000        match self {
1001            ActivityExecutionError::Failed(err) => Some(err.failure()),
1002            ActivityExecutionError::Cancelled(err) => Some(err.failure()),
1003            ActivityExecutionError::Serialization(_) => None,
1004        }
1005    }
1006
1007    /// Returns the normalized cause of the top-level activity failure, if any.
1008    pub fn cause(&self) -> Option<&IncomingError> {
1009        match self {
1010            ActivityExecutionError::Failed(err) => err.cause(),
1011            ActivityExecutionError::Cancelled(err) => err.cause(),
1012            ActivityExecutionError::Serialization(_) => None,
1013        }
1014    }
1015
1016    /// Returns the underlying failure reason for wrapper-shaped activity failures.
1017    pub fn reason(&self) -> Option<&IncomingError> {
1018        match self {
1019            ActivityExecutionError::Failed(err) => err.cause(),
1020            ActivityExecutionError::Cancelled(_) | ActivityExecutionError::Serialization(_) => None,
1021        }
1022    }
1023
1024    /// If this [`ActivityExecutionError`] was caused by a timeout, returns the associated
1025    /// [`TimeoutError`].
1026    pub fn as_timeout(&self) -> Option<&TimeoutError> {
1027        match self {
1028            ActivityExecutionError::Failed(err) => err.as_timeout(),
1029            ActivityExecutionError::Serialization(_) | ActivityExecutionError::Cancelled(_) => None,
1030        }
1031    }
1032
1033    /// If this [`ActivityExecutionError`] was caused by a cancellation, returns the associated
1034    /// [`CancelledError`].
1035    pub fn as_cancelled(&self) -> Option<&CancelledError> {
1036        match self {
1037            ActivityExecutionError::Failed(err) => err.as_cancelled(),
1038            ActivityExecutionError::Cancelled(err) => Some(err),
1039            ActivityExecutionError::Serialization(_) => None,
1040        }
1041    }
1042}
1043
1044/// Error returned when starting a child workflow fails.
1045#[derive(Debug, thiserror::Error)]
1046pub enum ChildWorkflowStartError {
1047    /// The child workflow start was cancelled before the normal execution wrapper path existed.
1048    #[error("Child workflow start cancelled: {}", .0.failure().message)]
1049    Cancelled(#[source] Box<CancelledError>),
1050    /// The child workflow failed to start (e.g., workflow ID already exists).
1051    #[error(
1052        "Child workflow start failed: workflow_id={workflow_id}, workflow_type={workflow_type}, cause={cause:?}"
1053    )]
1054    StartFailed {
1055        /// The workflow ID that was requested.
1056        workflow_id: String,
1057        /// The workflow type that was requested.
1058        workflow_type: String,
1059        /// The cause of the start failure.
1060        cause: StartChildWorkflowExecutionFailedCause,
1061    },
1062    /// Failed to serialize child workflow input payloads.
1063    #[error("Payload conversion failed: {0}")]
1064    Serialization(#[from] PayloadConversionError),
1065}
1066
1067impl ChildWorkflowStartError {
1068    /// Returns the retained top-level failure proto, if one exists.
1069    pub fn failure(&self) -> Option<&Failure> {
1070        match self {
1071            ChildWorkflowStartError::Cancelled(err) => Some(err.failure()),
1072            ChildWorkflowStartError::StartFailed { .. }
1073            | ChildWorkflowStartError::Serialization(_) => None,
1074        }
1075    }
1076
1077    /// Returns the normalized cause of the retained failure proto, if any.
1078    pub fn cause(&self) -> Option<&IncomingError> {
1079        match self {
1080            ChildWorkflowStartError::Cancelled(err) => err.cause(),
1081            ChildWorkflowStartError::StartFailed { .. }
1082            | ChildWorkflowStartError::Serialization(_) => None,
1083        }
1084    }
1085
1086    /// If this [`ChildWorkflowStartError`] was caused by cancellation, returns the associated
1087    /// [`CancelledError`].
1088    pub fn as_cancelled(&self) -> Option<&CancelledError> {
1089        match self {
1090            ChildWorkflowStartError::Cancelled(err) => Some(err),
1091            ChildWorkflowStartError::StartFailed { .. }
1092            | ChildWorkflowStartError::Serialization(_) => None,
1093        }
1094    }
1095}
1096
1097/// Error returned when a child workflow execution fails.
1098#[derive(Debug, thiserror::Error)]
1099pub enum ChildWorkflowExecutionError {
1100    /// The child workflow failed.
1101    #[error("Child workflow failed: {}", .0.failure().message)]
1102    Failed(#[source] Box<ChildWorkflowFailureError>),
1103    /// Failed to serialize input or deserialize the child workflow result payload.
1104    #[error("Payload conversion failed: {0}")]
1105    Serialization(#[from] PayloadConversionError),
1106}
1107
1108impl ChildWorkflowExecutionError {
1109    /// Returns the retained top-level child-workflow failure proto, if one exists.
1110    pub fn failure(&self) -> Option<&Failure> {
1111        match self {
1112            ChildWorkflowExecutionError::Failed(err) => Some(err.failure()),
1113            ChildWorkflowExecutionError::Serialization(_) => None,
1114        }
1115    }
1116
1117    /// Returns the normalized cause of the top-level child-workflow failure, if any.
1118    pub fn cause(&self) -> Option<&IncomingError> {
1119        match self {
1120            ChildWorkflowExecutionError::Failed(err) => err.cause(),
1121            ChildWorkflowExecutionError::Serialization(_) => None,
1122        }
1123    }
1124
1125    /// Returns the underlying failure reason for wrapper-shaped child-workflow failures.
1126    pub fn reason(&self) -> Option<&IncomingError> {
1127        match self {
1128            ChildWorkflowExecutionError::Failed(err) => err.cause(),
1129            ChildWorkflowExecutionError::Serialization(_) => None,
1130        }
1131    }
1132
1133    /// If this [`ChildWorkflowExecutionError`] was caused by a timeout, returns the associated
1134    /// [`TimeoutError`].
1135    pub fn as_timeout(&self) -> Option<&TimeoutError> {
1136        match self {
1137            ChildWorkflowExecutionError::Failed(err) => err.as_timeout(),
1138            ChildWorkflowExecutionError::Serialization(_) => None,
1139        }
1140    }
1141
1142    /// If this [`ChildWorkflowExecutionError`] was caused by a cancellation, returns the associated
1143    /// [`CancelledError`].
1144    pub fn as_cancelled(&self) -> Option<&CancelledError> {
1145        match self {
1146            ChildWorkflowExecutionError::Failed(err) => err.as_cancelled(),
1147            ChildWorkflowExecutionError::Serialization(_) => None,
1148        }
1149    }
1150}
1151
1152/// Error returned when signaling a workflow fails.
1153#[derive(Debug, thiserror::Error)]
1154pub enum WorkflowSignalError {
1155    /// The signal delivery failed.
1156    #[error("Child workflow signal failed: {}", .0.failure().message)]
1157    Failed(#[source] Box<WorkflowSignalFailureError>),
1158    /// Failed to serialize the signal input payload.
1159    #[error("Signal payload conversion failed: {0}")]
1160    Serialization(#[from] PayloadConversionError),
1161}
1162
1163impl WorkflowSignalError {
1164    /// Returns the retained top-level workflow signal failure proto, if one exists.
1165    pub fn failure(&self) -> Option<&Failure> {
1166        match self {
1167            WorkflowSignalError::Failed(err) => Some(err.failure()),
1168            WorkflowSignalError::Serialization(_) => None,
1169        }
1170    }
1171
1172    /// Returns the normalized cause of the workflow signal failure, if any.
1173    pub fn cause(&self) -> Option<&IncomingError> {
1174        match self {
1175            WorkflowSignalError::Failed(err) => err.cause(),
1176            WorkflowSignalError::Serialization(_) => None,
1177        }
1178    }
1179
1180    /// Returns the underlying failure reason for wrapper-shaped signal failures.
1181    pub fn reason(&self) -> Option<&IncomingError> {
1182        match self {
1183            WorkflowSignalError::Failed(err) => Some(err.error()),
1184            WorkflowSignalError::Serialization(_) => None,
1185        }
1186    }
1187
1188    /// If this [`WorkflowSignalError`] was caused by cancellation, returns the associated
1189    /// [`CancelledError`].
1190    pub fn as_cancelled(&self) -> Option<&CancelledError> {
1191        self.reason()?.as_cancelled()
1192    }
1193}
1194
1195/// A normalized workflow signal failure wrapper.
1196#[derive(Debug)]
1197pub struct WorkflowSignalFailureError {
1198    failure: Failure,
1199    error: Box<IncomingError>,
1200}
1201
1202impl WorkflowSignalFailureError {
1203    /// Creates a workflow signal failure wrapper.
1204    pub(crate) fn new(failure: Failure, error: IncomingError) -> Self {
1205        Self {
1206            failure,
1207            error: Box::new(error),
1208        }
1209    }
1210
1211    /// Returns the retained top-level proto failure.
1212    pub fn failure(&self) -> &Failure {
1213        &self.failure
1214    }
1215
1216    /// Returns the normalized direct cause of the workflow signal failure, if any.
1217    pub fn cause(&self) -> Option<&IncomingError> {
1218        self.error.cause()
1219    }
1220
1221    /// Returns the direct decoded incoming error represented by the top-level proto failure.
1222    pub fn error(&self) -> &IncomingError {
1223        &self.error
1224    }
1225}
1226
1227impl std::fmt::Display for WorkflowSignalFailureError {
1228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1229        self.failure.fmt(f)
1230    }
1231}
1232
1233impl std::error::Error for WorkflowSignalFailureError {
1234    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1235        self.cause()
1236            .map(|cause| cause as &(dyn std::error::Error + 'static))
1237    }
1238}
1239
1240#[cfg(test)]
1241mod tests {
1242    use super::*;
1243    use crate::{
1244        data_converters::{
1245            DefaultFailureConverter, FailureConverter, GenericPayloadConverter, PayloadConverter,
1246            SerializationContext, SerializationContextData,
1247        },
1248        protos::temporal::api::{
1249            common::v1::Payload,
1250            failure::v1::{ActivityFailureInfo, TimeoutFailureInfo, failure::FailureInfo},
1251        },
1252    };
1253
1254    struct AlwaysFailsSerialize;
1255
1256    impl serde::Serialize for AlwaysFailsSerialize {
1257        fn serialize<S: serde::Serializer>(&self, _serializer: S) -> Result<S::Ok, S::Error> {
1258            Err(serde::ser::Error::custom("serialize boom"))
1259        }
1260    }
1261
1262    #[test]
1263    fn decoded_failures_hide_unknown_state_values() {
1264        let failure = Failure {
1265            cause: Some(Box::new(Failure {
1266                failure_info: Some(FailureInfo::TimeoutFailureInfo(TimeoutFailureInfo {
1267                    timeout_type: 654_321,
1268                    ..Default::default()
1269                })),
1270                ..Default::default()
1271            })),
1272            failure_info: Some(FailureInfo::ActivityFailureInfo(ActivityFailureInfo {
1273                retry_state: 123_456,
1274                ..Default::default()
1275            })),
1276            ..Default::default()
1277        };
1278
1279        let decoded = DefaultFailureConverter
1280            .to_error(
1281                failure,
1282                &PayloadConverter::default(),
1283                &SerializationContextData::Workflow,
1284            )
1285            .unwrap();
1286        let IncomingError::Activity(activity) = decoded else {
1287            panic!("expected activity failure");
1288        };
1289        assert_eq!(activity.retry_state(), RetryState::Unknown);
1290        let Some(FailureInfo::ActivityFailureInfo(raw_activity)) =
1291            activity.failure().failure_info.as_ref()
1292        else {
1293            panic!("expected raw activity failure info");
1294        };
1295        assert_eq!(raw_activity.retry_state, 123_456);
1296        let Some(IncomingError::Timeout(timeout)) = activity.cause() else {
1297            panic!("expected timeout cause");
1298        };
1299        assert_eq!(timeout.timeout_type(), TimeoutType::Unknown);
1300        let Some(FailureInfo::TimeoutFailureInfo(raw_timeout)) =
1301            timeout.failure().failure_info.as_ref()
1302        else {
1303            panic!("expected raw timeout failure info");
1304        };
1305        assert_eq!(raw_timeout.timeout_type, 654_321);
1306    }
1307
1308    #[test]
1309    fn constructors_set_retryability_defaults() {
1310        assert!(!ApplicationFailure::new(anyhow::anyhow!("retryable")).is_non_retryable());
1311        assert!(
1312            ApplicationFailure::non_retryable(anyhow::anyhow!("non-retryable")).is_non_retryable()
1313        );
1314    }
1315
1316    #[test]
1317    fn conversion_preserves_application_metadata() {
1318        let payloads = Payloads {
1319            payloads: vec![Payload {
1320                data: b"details".to_vec(),
1321                ..Default::default()
1322            }],
1323        };
1324        let failure = DefaultFailureConverter.to_failure(
1325            OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
1326                ApplicationFailure::builder(anyhow::anyhow!("oops"))
1327                    .type_name("MyType".to_owned())
1328                    .non_retryable(true)
1329                    .next_retry_delay(Duration::from_secs(3))
1330                    .category(ApplicationErrorCategory::Benign)
1331                    .details(RawValue::new(payloads.payloads.clone()))
1332                    .build(),
1333            ))),
1334            &PayloadConverter::default(),
1335            &SerializationContextData::None,
1336        );
1337        let Some(FailureInfo::ApplicationFailureInfo(info)) = failure.failure_info else {
1338            panic!("expected application failure info");
1339        };
1340        assert_eq!(failure.message, "oops");
1341        assert_eq!(info.r#type, "MyType");
1342        assert!(info.non_retryable);
1343        assert_eq!(info.details, Some(payloads));
1344        assert_eq!(info.category(), ProtoApplicationErrorCategory::Benign);
1345        assert_eq!(info.next_retry_delay.unwrap().seconds, 3);
1346    }
1347
1348    #[test]
1349    fn builder_accepts_raw_payload_details() {
1350        let payload = Payload {
1351            data: b"details".to_vec(),
1352            ..Default::default()
1353        };
1354        let failure = DefaultFailureConverter.to_failure(
1355            OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
1356                ApplicationFailure::builder(anyhow::anyhow!("oops"))
1357                    .details(RawValue::new(vec![payload.clone()]))
1358                    .build(),
1359            ))),
1360            &PayloadConverter::default(),
1361            &SerializationContextData::None,
1362        );
1363
1364        let Some(FailureInfo::ApplicationFailureInfo(info)) = failure.failure_info else {
1365            panic!("expected application failure info");
1366        };
1367        assert_eq!(info.details.unwrap().payloads, vec![payload]);
1368    }
1369
1370    #[test]
1371    fn builder_accepts_serializable_details() {
1372        let failure = DefaultFailureConverter.to_failure(
1373            OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
1374                ApplicationFailure::builder(anyhow::anyhow!("oops"))
1375                    .details("details".to_string())
1376                    .build(),
1377            ))),
1378            &PayloadConverter::default(),
1379            &SerializationContextData::None,
1380        );
1381
1382        let Some(FailureInfo::ApplicationFailureInfo(info)) = failure.failure_info else {
1383            panic!("expected application failure info");
1384        };
1385        let payloads = info.details.expect("expected details").payloads;
1386        let converter = PayloadConverter::default();
1387        let details: String = converter
1388            .from_payloads(
1389                &SerializationContext {
1390                    data: &SerializationContextData::None,
1391                    converter: &converter,
1392                },
1393                payloads,
1394            )
1395            .unwrap();
1396        assert_eq!(details, "details");
1397    }
1398
1399    #[test]
1400    fn application_failure_encoding_surfaces_detail_encoding_errors() {
1401        let failure = DefaultFailureConverter.to_failure(
1402            OutgoingError::Workflow(OutgoingWorkflowError::Application(Box::new(
1403                ApplicationFailure::builder(anyhow::anyhow!("oops"))
1404                    .details(AlwaysFailsSerialize)
1405                    .build(),
1406            ))),
1407            &PayloadConverter::default(),
1408            &SerializationContextData::None,
1409        );
1410
1411        assert_eq!(
1412            failure.message,
1413            "Failed converting error to failure: Encoding error: serialize boom, original error message: oops"
1414        );
1415        assert!(matches!(
1416            failure.failure_info,
1417            Some(FailureInfo::ApplicationFailureInfo(_))
1418        ));
1419    }
1420
1421    #[test]
1422    fn anyhow_workflow_errors_default_to_application_outgoing_errors() {
1423        let outgoing: OutgoingWorkflowError = anyhow::anyhow!("workflow boom").into();
1424
1425        let OutgoingWorkflowError::Application(app) = outgoing else {
1426            panic!("plain workflow errors should default to application failures");
1427        };
1428        assert_eq!(app.to_string(), "workflow boom");
1429    }
1430
1431    #[test]
1432    fn payload_conversion_errors_use_dedicated_outgoing_variant() {
1433        let outgoing: OutgoingWorkflowError =
1434            PayloadConversionError::EncodingError(anyhow::anyhow!("encode boom").into()).into();
1435
1436        let OutgoingWorkflowError::PayloadConversion(err) = outgoing else {
1437            panic!("expected a payload conversion failure");
1438        };
1439        assert_eq!(err.to_string(), "Encoding error: encode boom");
1440    }
1441}