#[cfg(feature = "experimental")]
use crate::PluginApplyError;
use crate::{WorkflowExecutionStatus, workflow_handle::WorkflowResultDetails};
use http::uri::InvalidUri;
use temporalio_common::{
data_converters::{DecodablePayloads, PayloadConversionError},
error::{IncomingError, TimeoutType},
protos::{
google::rpc::Status as RpcStatus,
temporal::api::{
errordetails::v1::{
ActivityExecutionAlreadyStartedFailure, MultiOperationExecutionFailure,
WorkflowExecutionAlreadyStartedFailure,
multi_operation_execution_failure::OperationStatus,
},
failure::v1::Failure,
},
utilities::{decode_status_detail, encode_status_details},
},
};
use tonic::Code;
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum ClientConnectError {
#[cfg(feature = "experimental")]
#[error(transparent)]
Plugin(#[from] PluginApplyError),
#[error("Invalid URI: {0:?}")]
InvalidUri(#[from] InvalidUri),
#[error("Invalid headers: {0}")]
InvalidHeaders(#[from] InvalidHeaderError),
#[error("Server connection error: {0:?}")]
TonicTransportError(#[from] tonic::transport::Error),
#[error("`get_system_info` call error after connection: {0:?}")]
SystemInfoCallError(tonic::Status),
#[error("DNS resolution error for '{host}': {source}")]
DnsResolutionError {
host: String,
#[source]
source: std::io::Error,
},
#[error("Invalid client configuration: {0}")]
InvalidConfig(String),
}
impl From<ClientNewError> for ClientConnectError {
fn from(value: ClientNewError) -> Self {
match value {
#[cfg(feature = "experimental")]
ClientNewError::Plugin(err) => Self::Plugin(err),
}
}
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum InvalidHeaderError {
#[error("Invalid binary header key '{key}': {source}")]
InvalidBinaryHeaderKey {
key: String,
source: tonic::metadata::errors::InvalidMetadataKey,
},
#[error("Invalid ASCII header key '{key}': {source}")]
InvalidAsciiHeaderKey {
key: String,
source: tonic::metadata::errors::InvalidMetadataKey,
},
#[error("Invalid ASCII header value for key '{key}': {source}")]
InvalidAsciiHeaderValue {
key: String,
value: String,
source: tonic::metadata::errors::InvalidMetadataValue,
},
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum WorkflowStartError {
#[error("Workflow already started with run ID: {run_id:?}")]
AlreadyStarted {
run_id: Option<String>,
#[source]
source: tonic::Status,
},
#[error("Failed to serialize workflow input: {0}")]
PayloadConversion(#[from] PayloadConversionError),
#[error("Server error: {0}")]
Rpc(#[from] tonic::Status),
}
impl WorkflowStartError {
pub(crate) fn from_status(status: tonic::Status) -> Self {
if status.code() == Code::AlreadyExists {
let run_id =
decode_status_detail::<WorkflowExecutionAlreadyStartedFailure>(status.details())
.map(|failure| failure.run_id);
Self::AlreadyStarted {
run_id,
source: status,
}
} else {
Self::Rpc(status)
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WorkflowQueryError {
#[error("Workflow not found")]
NotFound(#[source] tonic::Status),
#[error("Query rejected: workflow status {status:?}")]
Rejected {
status: Option<WorkflowExecutionStatus>,
},
#[error("Payload conversion error: {0}")]
PayloadConversion(#[from] PayloadConversionError),
#[error("Server error: {0}")]
Rpc(tonic::Status),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl WorkflowQueryError {
pub(crate) fn from_status(status: tonic::Status) -> Self {
if status.code() == Code::NotFound {
Self::NotFound(status)
} else {
Self::Rpc(status)
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WorkflowUpdateError {
#[error("Workflow not found")]
NotFound(#[source] tonic::Status),
#[error("Update failed: {0:?}")]
Failed(Box<Failure>),
#[error("Payload conversion error: {0}")]
PayloadConversion(#[from] PayloadConversionError),
#[error("Server error: {0}")]
Rpc(tonic::Status),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl WorkflowUpdateError {
pub(crate) fn from_status(status: tonic::Status) -> Self {
if status.code() == Code::NotFound {
Self::NotFound(status)
} else {
Self::Rpc(status)
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WorkflowUpdateWithStartError {
#[error("Workflow start failed: {0}")]
Start(#[source] WorkflowStartError),
#[error("Workflow update failed: {0}")]
Update(#[source] WorkflowUpdateError),
#[error("Payload conversion error: {0}")]
PayloadConversion(#[from] PayloadConversionError),
#[error("Server error: {0}")]
Rpc(tonic::Status),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
const MULTI_OPERATION_ABORTED_NAME: &str = "temporal.api.failure.v1.MultiOperationExecutionAborted";
fn operation_status_to_tonic(op_status: OperationStatus) -> tonic::Status {
let code = Code::from(op_status.code);
let details = encode_status_details(&RpcStatus {
code: op_status.code,
message: op_status.message.clone(),
details: op_status.details,
});
tonic::Status::with_details(code, op_status.message, details.into())
}
impl WorkflowUpdateWithStartError {
pub(crate) fn from_status(status: tonic::Status) -> Self {
let Some(failure) =
decode_status_detail::<MultiOperationExecutionFailure>(status.details())
else {
return Self::Rpc(status);
};
let culprit = failure
.statuses
.into_iter()
.enumerate()
.find(|(_, op_status)| {
op_status.code != Code::Ok as i32
&& !op_status
.details
.iter()
.any(|detail| detail.type_url.ends_with(MULTI_OPERATION_ABORTED_NAME))
});
match culprit {
Some((0, op_status)) => Self::Start(WorkflowStartError::from_status(
operation_status_to_tonic(op_status),
)),
Some((_, op_status)) => Self::Update(WorkflowUpdateError::from_status(
operation_status_to_tonic(op_status),
)),
None => Self::Rpc(status),
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WorkflowGetResultError {
#[error("Workflow failed: {0}")]
Failed(#[source] Box<IncomingError>),
#[error("Workflow cancelled")]
Cancelled {
details: WorkflowResultDetails,
},
#[error("Workflow terminated")]
Terminated {
details: WorkflowResultDetails,
},
#[error("Workflow timed out")]
TimedOut,
#[error("Workflow continued as new")]
ContinuedAsNew,
#[error("Workflow not found")]
NotFound(#[source] tonic::Status),
#[error("Payload conversion error: {0}")]
PayloadConversion(#[from] PayloadConversionError),
#[error("Server error: {0}")]
Rpc(tonic::Status),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl From<WorkflowInteractionError> for WorkflowGetResultError {
fn from(err: WorkflowInteractionError) -> Self {
match err {
WorkflowInteractionError::NotFound(s) => Self::NotFound(s),
WorkflowInteractionError::PayloadConversion(e) => Self::PayloadConversion(e),
WorkflowInteractionError::Rpc(s) => Self::Rpc(s),
WorkflowInteractionError::Other(e) => Self::Other(e),
}
}
}
impl WorkflowGetResultError {
pub fn is_workflow_outcome(&self) -> bool {
matches!(
self,
Self::Failed(_)
| Self::Cancelled { .. }
| Self::Terminated { .. }
| Self::TimedOut
| Self::ContinuedAsNew
)
}
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum ClientError {
#[error("Payload conversion error: {0}")]
PayloadConversion(#[from] PayloadConversionError),
#[error("Server error: {0}")]
Rpc(#[from] tonic::Status),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum WorkflowInteractionError {
#[error("Workflow not found")]
NotFound(#[source] tonic::Status),
#[error("Payload conversion error: {0}")]
PayloadConversion(#[from] PayloadConversionError),
#[error("Server error: {0}")]
Rpc(tonic::Status),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl WorkflowInteractionError {
pub(crate) fn from_status(status: tonic::Status) -> Self {
if status.code() == Code::NotFound {
Self::NotFound(status)
} else {
Self::Rpc(status)
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum AsyncActivityError {
#[error("Activity not found")]
NotFound(#[source] tonic::Status),
#[error("Payload conversion error: {0}")]
PayloadConversion(#[from] PayloadConversionError),
#[error("Server error: {0}")]
Rpc(#[from] tonic::Status),
}
impl AsyncActivityError {
pub(crate) fn from_status(status: tonic::Status) -> Self {
if status.code() == Code::NotFound {
Self::NotFound(status)
} else {
Self::Rpc(status)
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ClientNewError {
#[cfg(feature = "experimental")]
#[error(transparent)]
Plugin(#[from] PluginApplyError),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ActivityInteractionError {
#[error("Activity not found")]
NotFound(#[source] tonic::Status),
#[error("Payload conversion error: {0}")]
PayloadConversion(#[from] PayloadConversionError),
#[error("Server error: {0}")]
Rpc(#[source] tonic::Status),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl From<tonic::Status> for ActivityInteractionError {
fn from(status: tonic::Status) -> Self {
if status.code() == Code::NotFound {
Self::NotFound(status)
} else {
Self::Rpc(status)
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum StartActivityError {
#[error("Activity already started with run_id={run_id}")]
AlreadyStarted {
run_id: String,
#[source]
source: tonic::Status,
},
#[error("Payload conversion error: {0}")]
PayloadConversion(#[from] PayloadConversionError),
#[error("Server error: {0}")]
Rpc(#[source] tonic::Status),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl From<tonic::Status> for StartActivityError {
fn from(status: tonic::Status) -> Self {
if status.code() == tonic::Code::AlreadyExists
&& let Some(details) =
decode_status_detail::<ActivityExecutionAlreadyStartedFailure>(status.details())
{
StartActivityError::AlreadyStarted {
run_id: details.run_id,
source: status,
}
} else {
StartActivityError::Rpc(status)
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ActivityResultError {
#[error("Activity failed: {0}")]
ActivityFailed(#[source] IncomingError),
#[error("Activity canceled")]
Cancelled {
details: DecodablePayloads,
},
#[error("Activity terminated")]
Terminated,
#[error("Activity timed out: {0:?}")]
TimedOut(TimeoutType),
#[error("Activity not found")]
NotFound(#[source] tonic::Status),
#[error("Payload conversion error: {0}")]
PayloadConversion(#[from] PayloadConversionError),
#[error("Server error: {0}")]
Rpc(#[source] tonic::Status),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl From<tonic::Status> for ActivityResultError {
fn from(status: tonic::Status) -> Self {
if status.code() == Code::NotFound {
Self::NotFound(status)
} else {
Self::Rpc(status)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use prost::Message;
use temporalio_common::protos::{
temporal::api::{
errordetails::v1::NotFoundFailure, failure::v1::MultiOperationExecutionAborted,
},
utilities::pack_any,
};
fn multi_op_status(code: Code, statuses: Vec<OperationStatus>) -> tonic::Status {
let failure = MultiOperationExecutionFailure { statuses };
let rpc_status = RpcStatus {
code: code as i32,
message: "multi-op failure".to_owned(),
details: vec![
pack_any(
"type.googleapis.com/temporal.api.errordetails.v1.MultiOperationExecutionFailure"
.to_owned(),
&failure,
)
.unwrap(),
],
};
tonic::Status::with_details(code, "multi-op failure", rpc_status.encode_to_vec().into())
}
fn aborted_status() -> OperationStatus {
OperationStatus {
code: Code::Aborted as i32,
message: "aborted".to_owned(),
details: vec![
pack_any(
"type.googleapis.com/temporal.api.failure.v1.MultiOperationExecutionAborted"
.to_owned(),
&MultiOperationExecutionAborted {},
)
.unwrap(),
],
}
}
#[test]
fn update_with_start_error_attributes_start_already_started() {
let status = multi_op_status(
Code::AlreadyExists,
vec![
OperationStatus {
code: Code::AlreadyExists as i32,
message: "already started".to_owned(),
details: vec![
pack_any(
"type.googleapis.com/temporal.api.errordetails.v1.WorkflowExecutionAlreadyStartedFailure"
.to_owned(),
&WorkflowExecutionAlreadyStartedFailure {
run_id: "existing-run".to_owned(),
..Default::default()
},
)
.unwrap(),
],
},
aborted_status(),
],
);
let err = WorkflowUpdateWithStartError::from_status(status);
assert_matches!(
err,
WorkflowUpdateWithStartError::Start(WorkflowStartError::AlreadyStarted {
run_id: Some(run_id),
..
}) if run_id == "existing-run"
);
}
#[test]
fn update_with_start_error_attributes_update_failure() {
let status = multi_op_status(
Code::NotFound,
vec![
aborted_status(),
OperationStatus {
code: Code::NotFound as i32,
message: "no such workflow".to_owned(),
details: vec![
pack_any(
"type.googleapis.com/temporal.api.errordetails.v1.NotFoundFailure"
.to_owned(),
&NotFoundFailure {
current_cluster: "here".to_owned(),
..Default::default()
},
)
.unwrap(),
],
},
],
);
let err = WorkflowUpdateWithStartError::from_status(status);
let inner = assert_matches!(
err,
WorkflowUpdateWithStartError::Update(WorkflowUpdateError::NotFound(status)) => status
);
assert_eq!(inner.message(), "no such workflow");
let detail = decode_status_detail::<NotFoundFailure>(inner.details())
.expect("operation details must be preserved");
assert_eq!(detail.current_cluster, "here");
}
#[test]
fn update_with_start_error_skips_successful_start() {
let status = multi_op_status(
Code::NotFound,
vec![
OperationStatus {
code: Code::Ok as i32,
message: String::new(),
details: vec![],
},
OperationStatus {
code: Code::NotFound as i32,
message: "update failed".to_owned(),
details: vec![],
},
],
);
let err = WorkflowUpdateWithStartError::from_status(status);
assert_matches!(
err,
WorkflowUpdateWithStartError::Update(WorkflowUpdateError::NotFound(status))
if status.message() == "update failed"
);
}
#[test]
fn update_with_start_error_without_details_is_rpc() {
let err =
WorkflowUpdateWithStartError::from_status(tonic::Status::new(Code::Internal, "boom"));
assert_matches!(err, WorkflowUpdateWithStartError::Rpc(status) if status.code() == Code::Internal);
}
}