1#[cfg(feature = "experimental")]
4use crate::PluginApplyError;
5use crate::{WorkflowExecutionStatus, workflow_handle::WorkflowResultDetails};
6use http::uri::InvalidUri;
7use temporalio_common::{
8 data_converters::{DecodablePayloads, PayloadConversionError},
9 error::{IncomingError, TimeoutType},
10 protos::{
11 google::rpc::Status as RpcStatus,
12 temporal::api::{
13 errordetails::v1::{
14 ActivityExecutionAlreadyStartedFailure, MultiOperationExecutionFailure,
15 WorkflowExecutionAlreadyStartedFailure,
16 multi_operation_execution_failure::OperationStatus,
17 },
18 failure::v1::Failure,
19 },
20 utilities::{decode_status_detail, encode_status_details},
21 },
22};
23use tonic::Code;
24
25#[derive(thiserror::Error, Debug)]
27#[non_exhaustive]
28pub enum ClientConnectError {
29 #[cfg(feature = "experimental")]
31 #[error(transparent)]
32 Plugin(#[from] PluginApplyError),
33 #[error("Invalid URI: {0:?}")]
35 InvalidUri(#[from] InvalidUri),
36 #[error("Invalid headers: {0}")]
38 InvalidHeaders(#[from] InvalidHeaderError),
39 #[error("Server connection error: {0:?}")]
41 TonicTransportError(#[from] tonic::transport::Error),
42 #[error("`get_system_info` call error after connection: {0:?}")]
45 SystemInfoCallError(tonic::Status),
46 #[error("DNS resolution error for '{host}': {source}")]
48 DnsResolutionError {
49 host: String,
51 #[source]
53 source: std::io::Error,
54 },
55 #[error("Invalid client configuration: {0}")]
57 InvalidConfig(String),
58}
59
60impl From<ClientNewError> for ClientConnectError {
61 fn from(value: ClientNewError) -> Self {
62 match value {
63 #[cfg(feature = "experimental")]
64 ClientNewError::Plugin(err) => Self::Plugin(err),
65 }
66 }
67}
68
69#[derive(thiserror::Error, Debug)]
71#[non_exhaustive]
72pub enum InvalidHeaderError {
73 #[error("Invalid binary header key '{key}': {source}")]
75 InvalidBinaryHeaderKey {
76 key: String,
78 source: tonic::metadata::errors::InvalidMetadataKey,
80 },
81 #[error("Invalid ASCII header key '{key}': {source}")]
83 InvalidAsciiHeaderKey {
84 key: String,
86 source: tonic::metadata::errors::InvalidMetadataKey,
88 },
89 #[error("Invalid ASCII header value for key '{key}': {source}")]
91 InvalidAsciiHeaderValue {
92 key: String,
94 value: String,
96 source: tonic::metadata::errors::InvalidMetadataValue,
98 },
99}
100
101#[derive(thiserror::Error, Debug)]
103#[non_exhaustive]
104pub enum WorkflowStartError {
105 #[error("Workflow already started with run ID: {run_id:?}")]
107 AlreadyStarted {
108 run_id: Option<String>,
110 #[source]
112 source: tonic::Status,
113 },
114 #[error("Failed to serialize workflow input: {0}")]
116 PayloadConversion(#[from] PayloadConversionError),
117 #[error("Server error: {0}")]
119 Rpc(#[from] tonic::Status),
120}
121
122impl WorkflowStartError {
123 pub(crate) fn from_status(status: tonic::Status) -> Self {
124 if status.code() == Code::AlreadyExists {
125 let run_id =
126 decode_status_detail::<WorkflowExecutionAlreadyStartedFailure>(status.details())
127 .map(|failure| failure.run_id);
128 Self::AlreadyStarted {
129 run_id,
130 source: status,
131 }
132 } else {
133 Self::Rpc(status)
134 }
135 }
136}
137
138#[derive(Debug, thiserror::Error)]
140#[non_exhaustive]
141pub enum WorkflowQueryError {
142 #[error("Workflow not found")]
144 NotFound(#[source] tonic::Status),
145
146 #[error("Query rejected: workflow status {status:?}")]
148 Rejected {
149 status: Option<WorkflowExecutionStatus>,
151 },
152
153 #[error("Payload conversion error: {0}")]
155 PayloadConversion(#[from] PayloadConversionError),
156
157 #[error("Server error: {0}")]
159 Rpc(tonic::Status),
160
161 #[error(transparent)]
163 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
164}
165
166impl WorkflowQueryError {
167 pub(crate) fn from_status(status: tonic::Status) -> Self {
168 if status.code() == Code::NotFound {
169 Self::NotFound(status)
170 } else {
171 Self::Rpc(status)
172 }
173 }
174}
175
176#[derive(Debug, thiserror::Error)]
178#[non_exhaustive]
179pub enum WorkflowUpdateError {
180 #[error("Workflow not found")]
182 NotFound(#[source] tonic::Status),
183
184 #[error("Update failed: {0:?}")]
186 Failed(Box<Failure>),
187
188 #[error("Payload conversion error: {0}")]
190 PayloadConversion(#[from] PayloadConversionError),
191
192 #[error("Server error: {0}")]
194 Rpc(tonic::Status),
195
196 #[error(transparent)]
198 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
199}
200
201impl WorkflowUpdateError {
202 pub(crate) fn from_status(status: tonic::Status) -> Self {
203 if status.code() == Code::NotFound {
204 Self::NotFound(status)
205 } else {
206 Self::Rpc(status)
207 }
208 }
209}
210
211#[derive(Debug, thiserror::Error)]
214#[non_exhaustive]
215pub enum WorkflowUpdateWithStartError {
216 #[error("Workflow start failed: {0}")]
218 Start(#[source] WorkflowStartError),
219
220 #[error("Workflow update failed: {0}")]
222 Update(#[source] WorkflowUpdateError),
223
224 #[error("Payload conversion error: {0}")]
226 PayloadConversion(#[from] PayloadConversionError),
227
228 #[error("Server error: {0}")]
230 Rpc(tonic::Status),
231
232 #[error(transparent)]
234 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
235}
236
237const MULTI_OPERATION_ABORTED_NAME: &str = "temporal.api.failure.v1.MultiOperationExecutionAborted";
238
239fn operation_status_to_tonic(op_status: OperationStatus) -> tonic::Status {
242 let code = Code::from(op_status.code);
243 let details = encode_status_details(&RpcStatus {
244 code: op_status.code,
245 message: op_status.message.clone(),
246 details: op_status.details,
247 });
248 tonic::Status::with_details(code, op_status.message, details.into())
249}
250
251impl WorkflowUpdateWithStartError {
252 pub(crate) fn from_status(status: tonic::Status) -> Self {
256 let Some(failure) =
257 decode_status_detail::<MultiOperationExecutionFailure>(status.details())
258 else {
259 return Self::Rpc(status);
260 };
261 let culprit = failure
262 .statuses
263 .into_iter()
264 .enumerate()
265 .find(|(_, op_status)| {
266 op_status.code != Code::Ok as i32
267 && !op_status
268 .details
269 .iter()
270 .any(|detail| detail.type_url.ends_with(MULTI_OPERATION_ABORTED_NAME))
271 });
272 match culprit {
273 Some((0, op_status)) => Self::Start(WorkflowStartError::from_status(
274 operation_status_to_tonic(op_status),
275 )),
276 Some((_, op_status)) => Self::Update(WorkflowUpdateError::from_status(
277 operation_status_to_tonic(op_status),
278 )),
279 None => Self::Rpc(status),
280 }
281 }
282}
283
284#[derive(Debug, thiserror::Error)]
286#[non_exhaustive]
287pub enum WorkflowGetResultError {
288 #[error("Workflow failed: {0}")]
290 Failed(#[source] Box<IncomingError>),
291
292 #[error("Workflow cancelled")]
294 Cancelled {
295 details: WorkflowResultDetails,
297 },
298
299 #[error("Workflow terminated")]
301 Terminated {
302 details: WorkflowResultDetails,
304 },
305
306 #[error("Workflow timed out")]
308 TimedOut,
309
310 #[error("Workflow continued as new")]
312 ContinuedAsNew,
313
314 #[error("Workflow not found")]
316 NotFound(#[source] tonic::Status),
317
318 #[error("Payload conversion error: {0}")]
320 PayloadConversion(#[from] PayloadConversionError),
321
322 #[error("Server error: {0}")]
324 Rpc(tonic::Status),
325
326 #[error(transparent)]
328 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
329}
330
331impl From<WorkflowInteractionError> for WorkflowGetResultError {
332 fn from(err: WorkflowInteractionError) -> Self {
333 match err {
334 WorkflowInteractionError::NotFound(s) => Self::NotFound(s),
335 WorkflowInteractionError::PayloadConversion(e) => Self::PayloadConversion(e),
336 WorkflowInteractionError::Rpc(s) => Self::Rpc(s),
337 WorkflowInteractionError::Other(e) => Self::Other(e),
338 }
339 }
340}
341
342impl WorkflowGetResultError {
343 pub fn is_workflow_outcome(&self) -> bool {
347 matches!(
348 self,
349 Self::Failed(_)
350 | Self::Cancelled { .. }
351 | Self::Terminated { .. }
352 | Self::TimedOut
353 | Self::ContinuedAsNew
354 )
355 }
356}
357
358#[derive(thiserror::Error, Debug)]
360#[non_exhaustive]
361pub enum ClientError {
362 #[error("Payload conversion error: {0}")]
364 PayloadConversion(#[from] PayloadConversionError),
365 #[error("Server error: {0}")]
367 Rpc(#[from] tonic::Status),
368}
369
370#[derive(Debug, thiserror::Error)]
373#[non_exhaustive]
374pub enum WorkflowInteractionError {
375 #[error("Workflow not found")]
377 NotFound(#[source] tonic::Status),
378
379 #[error("Payload conversion error: {0}")]
381 PayloadConversion(#[from] PayloadConversionError),
382
383 #[error("Server error: {0}")]
385 Rpc(tonic::Status),
386
387 #[error(transparent)]
389 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
390}
391
392impl WorkflowInteractionError {
393 pub(crate) fn from_status(status: tonic::Status) -> Self {
394 if status.code() == Code::NotFound {
395 Self::NotFound(status)
396 } else {
397 Self::Rpc(status)
398 }
399 }
400}
401
402#[derive(Debug, thiserror::Error)]
404#[non_exhaustive]
405pub enum AsyncActivityError {
406 #[error("Activity not found")]
408 NotFound(#[source] tonic::Status),
409 #[error("Payload conversion error: {0}")]
411 PayloadConversion(#[from] PayloadConversionError),
412 #[error("Server error: {0}")]
414 Rpc(#[from] tonic::Status),
415}
416
417impl AsyncActivityError {
418 pub(crate) fn from_status(status: tonic::Status) -> Self {
419 if status.code() == Code::NotFound {
420 Self::NotFound(status)
421 } else {
422 Self::Rpc(status)
423 }
424 }
425}
426
427#[derive(Debug, thiserror::Error)]
429#[non_exhaustive]
430pub enum ClientNewError {
431 #[cfg(feature = "experimental")]
433 #[error(transparent)]
434 Plugin(#[from] PluginApplyError),
435}
436
437#[derive(Debug, thiserror::Error)]
439#[non_exhaustive]
440pub enum ActivityInteractionError {
441 #[error("Activity not found")]
443 NotFound(#[source] tonic::Status),
444
445 #[error("Payload conversion error: {0}")]
447 PayloadConversion(#[from] PayloadConversionError),
448
449 #[error("Server error: {0}")]
451 Rpc(#[source] tonic::Status),
452
453 #[error(transparent)]
455 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
456}
457
458impl From<tonic::Status> for ActivityInteractionError {
459 fn from(status: tonic::Status) -> Self {
460 if status.code() == Code::NotFound {
461 Self::NotFound(status)
462 } else {
463 Self::Rpc(status)
464 }
465 }
466}
467
468#[allow(clippy::large_enum_variant)]
470#[derive(Debug, thiserror::Error)]
471#[non_exhaustive]
472pub enum StartActivityError {
473 #[error("Activity already started with run_id={run_id}")]
476 AlreadyStarted {
477 run_id: String,
479 #[source]
481 source: tonic::Status,
482 },
483
484 #[error("Payload conversion error: {0}")]
486 PayloadConversion(#[from] PayloadConversionError),
487
488 #[error("Server error: {0}")]
490 Rpc(#[source] tonic::Status),
491
492 #[error(transparent)]
494 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
495}
496
497impl From<tonic::Status> for StartActivityError {
498 fn from(status: tonic::Status) -> Self {
499 if status.code() == tonic::Code::AlreadyExists
500 && let Some(details) =
501 decode_status_detail::<ActivityExecutionAlreadyStartedFailure>(status.details())
502 {
503 StartActivityError::AlreadyStarted {
504 run_id: details.run_id,
505 source: status,
506 }
507 } else {
508 StartActivityError::Rpc(status)
509 }
510 }
511}
512
513#[allow(clippy::large_enum_variant)]
515#[derive(Debug, thiserror::Error)]
516#[non_exhaustive]
517pub enum ActivityResultError {
518 #[error("Activity failed: {0}")]
520 ActivityFailed(#[source] IncomingError),
521
522 #[error("Activity canceled")]
524 Cancelled {
525 details: DecodablePayloads,
527 },
528
529 #[error("Activity terminated")]
531 Terminated,
532
533 #[error("Activity timed out: {0:?}")]
535 TimedOut(TimeoutType),
536
537 #[error("Activity not found")]
539 NotFound(#[source] tonic::Status),
540
541 #[error("Payload conversion error: {0}")]
543 PayloadConversion(#[from] PayloadConversionError),
544
545 #[error("Server error: {0}")]
547 Rpc(#[source] tonic::Status),
548
549 #[error(transparent)]
551 Other(#[from] Box<dyn std::error::Error + Send + Sync>),
552}
553
554impl From<tonic::Status> for ActivityResultError {
555 fn from(status: tonic::Status) -> Self {
556 if status.code() == Code::NotFound {
557 Self::NotFound(status)
558 } else {
559 Self::Rpc(status)
560 }
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567 use assert_matches::assert_matches;
568 use prost::Message;
569 use temporalio_common::protos::{
570 temporal::api::{
571 errordetails::v1::NotFoundFailure, failure::v1::MultiOperationExecutionAborted,
572 },
573 utilities::pack_any,
574 };
575
576 fn multi_op_status(code: Code, statuses: Vec<OperationStatus>) -> tonic::Status {
577 let failure = MultiOperationExecutionFailure { statuses };
578 let rpc_status = RpcStatus {
579 code: code as i32,
580 message: "multi-op failure".to_owned(),
581 details: vec![
582 pack_any(
583 "type.googleapis.com/temporal.api.errordetails.v1.MultiOperationExecutionFailure"
584 .to_owned(),
585 &failure,
586 )
587 .unwrap(),
588 ],
589 };
590 tonic::Status::with_details(code, "multi-op failure", rpc_status.encode_to_vec().into())
591 }
592
593 fn aborted_status() -> OperationStatus {
594 OperationStatus {
595 code: Code::Aborted as i32,
596 message: "aborted".to_owned(),
597 details: vec![
598 pack_any(
599 "type.googleapis.com/temporal.api.failure.v1.MultiOperationExecutionAborted"
600 .to_owned(),
601 &MultiOperationExecutionAborted {},
602 )
603 .unwrap(),
604 ],
605 }
606 }
607
608 #[test]
609 fn update_with_start_error_attributes_start_already_started() {
610 let status = multi_op_status(
611 Code::AlreadyExists,
612 vec![
613 OperationStatus {
614 code: Code::AlreadyExists as i32,
615 message: "already started".to_owned(),
616 details: vec![
617 pack_any(
618 "type.googleapis.com/temporal.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure"
619 .to_owned(),
620 &WorkflowExecutionAlreadyStartedFailure {
621 run_id: "existing-run".to_owned(),
622 ..Default::default()
623 },
624 )
625 .unwrap(),
626 ],
627 },
628 aborted_status(),
629 ],
630 );
631
632 let err = WorkflowUpdateWithStartError::from_status(status);
633 assert_matches!(
634 err,
635 WorkflowUpdateWithStartError::Start(WorkflowStartError::AlreadyStarted {
636 run_id: Some(run_id),
637 ..
638 }) if run_id == "existing-run"
639 );
640 }
641
642 #[test]
643 fn update_with_start_error_attributes_update_failure() {
644 let status = multi_op_status(
645 Code::NotFound,
646 vec![
647 aborted_status(),
648 OperationStatus {
649 code: Code::NotFound as i32,
650 message: "no such workflow".to_owned(),
651 details: vec![
652 pack_any(
653 "type.googleapis.com/temporal.api.errordetails.v1.NotFoundFailure"
654 .to_owned(),
655 &NotFoundFailure {
656 current_cluster: "here".to_owned(),
657 ..Default::default()
658 },
659 )
660 .unwrap(),
661 ],
662 },
663 ],
664 );
665
666 let err = WorkflowUpdateWithStartError::from_status(status);
667 let inner = assert_matches!(
668 err,
669 WorkflowUpdateWithStartError::Update(WorkflowUpdateError::NotFound(status)) => status
670 );
671 assert_eq!(inner.message(), "no such workflow");
672 let detail = decode_status_detail::<NotFoundFailure>(inner.details())
674 .expect("operation details must be preserved");
675 assert_eq!(detail.current_cluster, "here");
676 }
677
678 #[test]
679 fn update_with_start_error_skips_successful_start() {
680 let status = multi_op_status(
681 Code::NotFound,
682 vec![
683 OperationStatus {
684 code: Code::Ok as i32,
685 message: String::new(),
686 details: vec![],
687 },
688 OperationStatus {
689 code: Code::NotFound as i32,
690 message: "update failed".to_owned(),
691 details: vec![],
692 },
693 ],
694 );
695
696 let err = WorkflowUpdateWithStartError::from_status(status);
697 assert_matches!(
698 err,
699 WorkflowUpdateWithStartError::Update(WorkflowUpdateError::NotFound(status))
700 if status.message() == "update failed"
701 );
702 }
703
704 #[test]
705 fn update_with_start_error_without_details_is_rpc() {
706 let err =
707 WorkflowUpdateWithStartError::from_status(tonic::Status::new(Code::Internal, "boom"));
708 assert_matches!(err, WorkflowUpdateWithStartError::Rpc(status) if status.code() == Code::Internal);
709 }
710}