Skip to main content

temporalio_client/
errors.rs

1//! Contains errors that can be returned by clients.
2
3use crate::{PluginApplyError, WorkflowExecutionStatus, workflow_handle::WorkflowResultDetails};
4use http::uri::InvalidUri;
5use temporalio_common::{
6    data_converters::{DecodablePayloads, PayloadConversionError},
7    error::{IncomingError, TimeoutType},
8    protos::{
9        temporal::api::{
10            errordetails::v1::ActivityExecutionAlreadyStartedFailure, failure::v1::Failure,
11        },
12        utilities::decode_status_detail,
13    },
14};
15use tonic::Code;
16
17/// Errors thrown while attempting to establish a connection to the server
18#[derive(thiserror::Error, Debug)]
19#[non_exhaustive]
20pub enum ClientConnectError {
21    /// A plugin failed while configuring connection options.
22    #[error(transparent)]
23    Plugin(#[from] PluginApplyError),
24    /// Invalid URI. Configuration error, fatal.
25    #[error("Invalid URI: {0:?}")]
26    InvalidUri(#[from] InvalidUri),
27    /// Invalid gRPC metadata headers. Configuration error.
28    #[error("Invalid headers: {0}")]
29    InvalidHeaders(#[from] InvalidHeaderError),
30    /// Server connection error. Crashing and restarting the worker is likely best.
31    #[error("Server connection error: {0:?}")]
32    TonicTransportError(#[from] tonic::transport::Error),
33    /// We couldn't successfully make the `get_system_info` call at connection time to establish
34    /// server capabilities / verify server is responding.
35    #[error("`get_system_info` call error after connection: {0:?}")]
36    SystemInfoCallError(tonic::Status),
37    /// DNS resolution failed when attempting load-balanced connection.
38    #[error("DNS resolution error for '{host}': {source}")]
39    DnsResolutionError {
40        /// The host that failed to resolve.
41        host: String,
42        /// The underlying IO error.
43        #[source]
44        source: std::io::Error,
45    },
46    /// Invalid client configuration.
47    #[error("Invalid client configuration: {0}")]
48    InvalidConfig(String),
49}
50
51impl From<ClientNewError> for ClientConnectError {
52    fn from(value: ClientNewError) -> Self {
53        match value {
54            ClientNewError::Plugin(err) => Self::Plugin(err),
55        }
56    }
57}
58
59/// Errors thrown when a gRPC metadata header is invalid.
60#[derive(thiserror::Error, Debug)]
61#[non_exhaustive]
62pub enum InvalidHeaderError {
63    /// A binary header key was invalid
64    #[error("Invalid binary header key '{key}': {source}")]
65    InvalidBinaryHeaderKey {
66        /// The invalid key
67        key: String,
68        /// The source error from tonic
69        source: tonic::metadata::errors::InvalidMetadataKey,
70    },
71    /// An ASCII header key was invalid
72    #[error("Invalid ASCII header key '{key}': {source}")]
73    InvalidAsciiHeaderKey {
74        /// The invalid key
75        key: String,
76        /// The source error from tonic
77        source: tonic::metadata::errors::InvalidMetadataKey,
78    },
79    /// An ASCII header value was invalid
80    #[error("Invalid ASCII header value for key '{key}': {source}")]
81    InvalidAsciiHeaderValue {
82        /// The key
83        key: String,
84        /// The invalid value
85        value: String,
86        /// The source error from tonic
87        source: tonic::metadata::errors::InvalidMetadataValue,
88    },
89}
90
91/// Errors that can occur when starting a workflow.
92#[derive(thiserror::Error, Debug)]
93#[non_exhaustive]
94pub enum WorkflowStartError {
95    /// The workflow already exists.
96    #[error("Workflow already started with run ID: {run_id:?}")]
97    AlreadyStarted {
98        /// Run ID of the already-started workflow if this was raised by the client.
99        run_id: Option<String>,
100        /// The original gRPC status from the server.
101        #[source]
102        source: tonic::Status,
103    },
104    /// Error converting the input to a payload.
105    #[error("Failed to serialize workflow input: {0}")]
106    PayloadConversion(#[from] PayloadConversionError),
107    /// An uncategorized rpc error from the server.
108    #[error("Server error: {0}")]
109    Rpc(#[from] tonic::Status),
110}
111
112/// Errors returned by query operations on [crate::WorkflowHandle].
113#[derive(Debug, thiserror::Error)]
114#[non_exhaustive]
115pub enum WorkflowQueryError {
116    /// The workflow was not found.
117    #[error("Workflow not found")]
118    NotFound(#[source] tonic::Status),
119
120    /// The query was rejected based on the rejection condition.
121    #[error("Query rejected: workflow status {status:?}")]
122    Rejected {
123        /// The workflow status that caused the query rejection, if reported.
124        status: Option<WorkflowExecutionStatus>,
125    },
126
127    /// Error serializing input or deserializing output.
128    #[error("Payload conversion error: {0}")]
129    PayloadConversion(#[from] PayloadConversionError),
130
131    /// An uncategorized RPC error from the server.
132    #[error("Server error: {0}")]
133    Rpc(tonic::Status),
134
135    /// Other errors.
136    #[error(transparent)]
137    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
138}
139
140impl WorkflowQueryError {
141    pub(crate) fn from_status(status: tonic::Status) -> Self {
142        if status.code() == Code::NotFound {
143            Self::NotFound(status)
144        } else {
145            Self::Rpc(status)
146        }
147    }
148}
149
150/// Errors returned by update operations on [crate::WorkflowHandle].
151#[derive(Debug, thiserror::Error)]
152#[non_exhaustive]
153pub enum WorkflowUpdateError {
154    /// The workflow was not found.
155    #[error("Workflow not found")]
156    NotFound(#[source] tonic::Status),
157
158    /// The update failed with an application-level failure.
159    #[error("Update failed: {0:?}")]
160    Failed(Box<Failure>),
161
162    /// Error serializing input or deserializing output.
163    #[error("Payload conversion error: {0}")]
164    PayloadConversion(#[from] PayloadConversionError),
165
166    /// An uncategorized RPC error from the server.
167    #[error("Server error: {0}")]
168    Rpc(tonic::Status),
169
170    /// Other errors.
171    #[error(transparent)]
172    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
173}
174
175impl WorkflowUpdateError {
176    pub(crate) fn from_status(status: tonic::Status) -> Self {
177        if status.code() == Code::NotFound {
178            Self::NotFound(status)
179        } else {
180            Self::Rpc(status)
181        }
182    }
183}
184
185/// Errors returned by workflow get_result operations.
186#[derive(Debug, thiserror::Error)]
187#[non_exhaustive]
188pub enum WorkflowGetResultError {
189    /// The workflow finished in failure.
190    #[error("Workflow failed: {0}")]
191    Failed(#[source] Box<IncomingError>),
192
193    /// The workflow was cancelled.
194    #[error("Workflow cancelled")]
195    Cancelled {
196        /// Details provided at cancellation time.
197        details: WorkflowResultDetails,
198    },
199
200    /// The workflow was terminated.
201    #[error("Workflow terminated")]
202    Terminated {
203        /// Details provided at termination time.
204        details: WorkflowResultDetails,
205    },
206
207    /// The workflow timed out.
208    #[error("Workflow timed out")]
209    TimedOut,
210
211    /// The workflow continued as new.
212    #[error("Workflow continued as new")]
213    ContinuedAsNew,
214
215    /// The workflow was not found.
216    #[error("Workflow not found")]
217    NotFound(#[source] tonic::Status),
218
219    /// Error serializing input or deserializing output.
220    #[error("Payload conversion error: {0}")]
221    PayloadConversion(#[from] PayloadConversionError),
222
223    /// An uncategorized RPC error from the server.
224    #[error("Server error: {0}")]
225    Rpc(tonic::Status),
226
227    /// Other errors.
228    #[error(transparent)]
229    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
230}
231
232impl From<WorkflowInteractionError> for WorkflowGetResultError {
233    fn from(err: WorkflowInteractionError) -> Self {
234        match err {
235            WorkflowInteractionError::NotFound(s) => Self::NotFound(s),
236            WorkflowInteractionError::PayloadConversion(e) => Self::PayloadConversion(e),
237            WorkflowInteractionError::Rpc(s) => Self::Rpc(s),
238            WorkflowInteractionError::Other(e) => Self::Other(e),
239        }
240    }
241}
242
243impl WorkflowGetResultError {
244    /// Returns `true` if this error represents a workflow-level non-success outcome
245    /// (Failed, Cancelled, Terminated, TimedOut, or ContinuedAsNew) rather than an
246    /// infrastructure/RPC error.
247    pub fn is_workflow_outcome(&self) -> bool {
248        matches!(
249            self,
250            Self::Failed(_)
251                | Self::Cancelled { .. }
252                | Self::Terminated { .. }
253                | Self::TimedOut
254                | Self::ContinuedAsNew
255        )
256    }
257}
258
259/// Errors returned by client methods that don't need more specific error types.
260#[derive(thiserror::Error, Debug)]
261#[non_exhaustive]
262pub enum ClientError {
263    /// Error decoding payloads returned by the server.
264    #[error("Payload conversion error: {0}")]
265    PayloadConversion(#[from] PayloadConversionError),
266    /// An uncategorized rpc error from the server.
267    #[error("Server error: {0}")]
268    Rpc(#[from] tonic::Status),
269}
270
271/// Errors returned by methods on [crate::WorkflowHandle] for general operations
272/// like signal, cancel, terminate, describe, fetch_history, and get_result.
273#[derive(Debug, thiserror::Error)]
274#[non_exhaustive]
275pub enum WorkflowInteractionError {
276    /// The workflow was not found.
277    #[error("Workflow not found")]
278    NotFound(#[source] tonic::Status),
279
280    /// Error serializing input or deserializing output.
281    #[error("Payload conversion error: {0}")]
282    PayloadConversion(#[from] PayloadConversionError),
283
284    /// An uncategorized RPC error from the server.
285    #[error("Server error: {0}")]
286    Rpc(tonic::Status),
287
288    /// Other errors.
289    #[error(transparent)]
290    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
291}
292
293impl WorkflowInteractionError {
294    pub(crate) fn from_status(status: tonic::Status) -> Self {
295        if status.code() == Code::NotFound {
296            Self::NotFound(status)
297        } else {
298            Self::Rpc(status)
299        }
300    }
301}
302
303/// Errors that can occur when completing an activity asynchronously.
304#[derive(Debug, thiserror::Error)]
305#[non_exhaustive]
306pub enum AsyncActivityError {
307    /// The activity was not found (e.g., already completed, cancelled, or never existed).
308    #[error("Activity not found")]
309    NotFound(#[source] tonic::Status),
310    /// Error serializing an activity result, failure, or details.
311    #[error("Payload conversion error: {0}")]
312    PayloadConversion(#[from] PayloadConversionError),
313    /// An uncategorized rpc error from the server.
314    #[error("Server error: {0}")]
315    Rpc(#[from] tonic::Status),
316}
317
318impl AsyncActivityError {
319    pub(crate) fn from_status(status: tonic::Status) -> Self {
320        if status.code() == Code::NotFound {
321            Self::NotFound(status)
322        } else {
323            Self::Rpc(status)
324        }
325    }
326}
327
328/// Errors that can occur when constructing a [`crate::Client`].
329#[derive(Debug, thiserror::Error)]
330#[non_exhaustive]
331pub enum ClientNewError {
332    /// A plugin failed while configuring client options.
333    #[error(transparent)]
334    Plugin(#[from] PluginApplyError),
335}
336
337/// Errors returned by methods on [crate::ActivityHandle] that don't need more specific error types.
338#[derive(Debug, thiserror::Error)]
339#[non_exhaustive]
340pub enum ActivityInteractionError {
341    /// The activity was not found.
342    #[error("Activity not found")]
343    NotFound(#[source] tonic::Status),
344
345    /// Error deserializing output.
346    #[error("Payload conversion error: {0}")]
347    PayloadConversion(#[from] PayloadConversionError),
348
349    /// An uncategorized RPC error from the server.
350    #[error("Server error: {0}")]
351    Rpc(#[source] tonic::Status),
352
353    /// Other errors.
354    #[error(transparent)]
355    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
356}
357
358impl From<tonic::Status> for ActivityInteractionError {
359    fn from(status: tonic::Status) -> Self {
360        if status.code() == Code::NotFound {
361            Self::NotFound(status)
362        } else {
363            Self::Rpc(status)
364        }
365    }
366}
367
368/// Errors that can occur when starting a standalone activity.
369#[allow(clippy::large_enum_variant)]
370#[derive(Debug, thiserror::Error)]
371#[non_exhaustive]
372pub enum StartActivityError {
373    /// There's a conflicting activity execution with the same ID according to chosen ID reuse
374    /// policy and ID conflict policy.
375    #[error("Activity already started with run_id={run_id}")]
376    AlreadyStarted {
377        /// Run ID of the existing execution with the same activity ID.
378        run_id: String,
379        /// Raw error from the server.
380        #[source]
381        source: tonic::Status,
382    },
383
384    /// Error serializing input.
385    #[error("Payload conversion error: {0}")]
386    PayloadConversion(#[from] PayloadConversionError),
387
388    /// An uncategorized RPC error from the server.
389    #[error("Server error: {0}")]
390    Rpc(#[source] tonic::Status),
391
392    /// Other errors.
393    #[error(transparent)]
394    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
395}
396
397impl From<tonic::Status> for StartActivityError {
398    fn from(status: tonic::Status) -> Self {
399        if status.code() == tonic::Code::AlreadyExists
400            && let Some(details) =
401                decode_status_detail::<ActivityExecutionAlreadyStartedFailure>(status.details())
402        {
403            StartActivityError::AlreadyStarted {
404                run_id: details.run_id,
405                source: status,
406            }
407        } else {
408            StartActivityError::Rpc(status)
409        }
410    }
411}
412
413/// Errors returned by [`crate::ActivityHandle::result`].
414#[allow(clippy::large_enum_variant)]
415#[derive(Debug, thiserror::Error)]
416#[non_exhaustive]
417pub enum ActivityResultError {
418    /// Activity execution did not complete successfully.
419    #[error("Activity failed: {0}")]
420    ActivityFailed(#[source] IncomingError),
421
422    /// The activity was canceled.
423    #[error("Activity canceled")]
424    Cancelled {
425        /// Details provided at cancellation time.
426        details: DecodablePayloads,
427    },
428
429    /// The workflow was terminated.
430    #[error("Activity terminated")]
431    Terminated,
432
433    /// The activity timed out.
434    #[error("Activity timed out: {0:?}")]
435    TimedOut(TimeoutType),
436
437    /// The activity was not found.
438    #[error("Activity not found")]
439    NotFound(#[source] tonic::Status),
440
441    /// Error deserializing output.
442    #[error("Payload conversion error: {0}")]
443    PayloadConversion(#[from] PayloadConversionError),
444
445    /// An uncategorized RPC error from the server.
446    #[error("Server error: {0}")]
447    Rpc(#[source] tonic::Status),
448
449    /// Other errors.
450    #[error(transparent)]
451    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
452}
453
454impl From<tonic::Status> for ActivityResultError {
455    fn from(status: tonic::Status) -> Self {
456        if status.code() == Code::NotFound {
457            Self::NotFound(status)
458        } else {
459            Self::Rpc(status)
460        }
461    }
462}