iota_rest_api/
error.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use axum::http::StatusCode;
6
7pub type Result<T, E = RestError> = std::result::Result<T, E>;
8
9pub struct RestError {
10    status: StatusCode,
11    message: Option<String>,
12}
13
14impl RestError {
15    pub fn new<T: Into<String>>(status: StatusCode, message: T) -> Self {
16        Self {
17            status,
18            message: Some(message.into()),
19        }
20    }
21}
22
23// Tell axum how to convert `AppError` into a response.
24impl axum::response::IntoResponse for RestError {
25    fn into_response(self) -> axum::response::Response {
26        match self.message {
27            Some(message) => (self.status, message).into_response(),
28            None => self.status.into_response(),
29        }
30    }
31}
32
33impl From<iota_types::storage::error::Error> for RestError {
34    fn from(value: iota_types::storage::error::Error) -> Self {
35        Self {
36            status: StatusCode::INTERNAL_SERVER_ERROR,
37            message: Some(value.to_string()),
38        }
39    }
40}
41
42impl From<anyhow::Error> for RestError {
43    fn from(value: anyhow::Error) -> Self {
44        Self {
45            status: StatusCode::INTERNAL_SERVER_ERROR,
46            message: Some(value.to_string()),
47        }
48    }
49}
50
51impl From<iota_types::iota_sdk2_conversions::SdkTypeConversionError> for RestError {
52    fn from(value: iota_types::iota_sdk2_conversions::SdkTypeConversionError) -> Self {
53        Self {
54            status: StatusCode::INTERNAL_SERVER_ERROR,
55            message: Some(value.to_string()),
56        }
57    }
58}
59
60impl From<bcs::Error> for RestError {
61    fn from(value: bcs::Error) -> Self {
62        Self {
63            status: StatusCode::INTERNAL_SERVER_ERROR,
64            message: Some(value.to_string()),
65        }
66    }
67}
68
69impl From<iota_types::quorum_driver_types::QuorumDriverError> for RestError {
70    fn from(error: iota_types::quorum_driver_types::QuorumDriverError) -> Self {
71        use iota_types::{error::IotaError, quorum_driver_types::QuorumDriverError::*};
72        use itertools::Itertools;
73
74        match error {
75            InvalidUserSignature(err) => {
76                let message = {
77                    let err = match err {
78                        IotaError::UserInput { error } => error.to_string(),
79                        _ => err.to_string(),
80                    };
81                    format!("Invalid user signature: {err}")
82                };
83
84                RestError::new(StatusCode::BAD_REQUEST, message)
85            }
86            QuorumDriverInternal(err) => {
87                RestError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
88            }
89            ObjectsDoubleUsed {
90                conflicting_txes,
91                retried_tx,
92                retried_tx_success,
93            } => {
94                let new_map = conflicting_txes
95                    .into_iter()
96                    .map(|(digest, (pairs, _))| {
97                        (
98                            digest,
99                            pairs.into_iter().map(|(_, obj_ref)| obj_ref).collect(),
100                        )
101                    })
102                    .collect::<std::collections::BTreeMap<_, Vec<_>>>();
103
104                let message = format!(
105                    "Failed to sign transaction by a quorum of validators because of locked objects. Retried a conflicting transaction {:?}, success: {:?}. Conflicting Transactions:\n{:#?}",
106                    retried_tx, retried_tx_success, new_map,
107                );
108
109                RestError::new(StatusCode::CONFLICT, message)
110            }
111            TimeoutBeforeFinality | FailedWithTransientErrorAfterMaximumAttempts { .. } => {
112                // TODO add a Retry-After header
113                RestError::new(
114                    StatusCode::SERVICE_UNAVAILABLE,
115                    "timed-out before finality could be reached",
116                )
117            }
118            NonRecoverableTransactionError { errors } => {
119                let new_errors: Vec<String> = errors
120                    .into_iter()
121                    // sort by total stake, descending, so users see the most prominent one first
122                    .sorted_by(|(_, a, _), (_, b, _)| b.cmp(a))
123                    .filter_map(|(err, _, _)| {
124                        match &err {
125                            // Special handling of UserInputError:
126                            // ObjectNotFound and DependentPackageNotFound are considered
127                            // retryable errors but they have different treatment
128                            // in AuthorityAggregator.
129                            // The optimal fix would be to examine if the total stake
130                            // of ObjectNotFound/DependentPackageNotFound exceeds the
131                            // quorum threshold, but it takes a Committee here.
132                            // So, we take an easier route and consider them non-retryable
133                            // at all. Combining this with the sorting above, clients will
134                            // see the dominant error first.
135                            IotaError::UserInput { error } => Some(error.to_string()),
136                            _ => {
137                                if err.is_retryable().0 {
138                                    None
139                                } else {
140                                    Some(err.to_string())
141                                }
142                            }
143                        }
144                    })
145                    .collect();
146
147                assert!(
148                    !new_errors.is_empty(),
149                    "NonRecoverableTransactionError should have at least one non-retryable error"
150                );
151
152                let error_list = new_errors.join(", ");
153                let error_msg = format!(
154                    "Transaction execution failed due to issues with transaction inputs, please review the errors and try again: {}.",
155                    error_list
156                );
157
158                RestError::new(StatusCode::BAD_REQUEST, error_msg)
159            }
160            TxAlreadyFinalizedWithDifferentUserSignatures => RestError::new(
161                StatusCode::CONFLICT,
162                "The transaction is already finalized but with different user signatures",
163            ),
164            SystemOverload { .. } | SystemOverloadRetryAfter { .. } => {
165                // TODO add a Retry-After header
166                RestError::new(StatusCode::SERVICE_UNAVAILABLE, "system is overloaded")
167            }
168        }
169    }
170}