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 { conflicting_txes } => {
90                let new_map = conflicting_txes
91                    .into_iter()
92                    .map(|(digest, (pairs, _))| {
93                        (
94                            digest,
95                            pairs.into_iter().map(|(_, obj_ref)| obj_ref).collect(),
96                        )
97                    })
98                    .collect::<std::collections::BTreeMap<_, Vec<_>>>();
99
100                let message = format!(
101                    "Failed to sign transaction by a quorum of validators because of locked objects. Conflicting Transactions:\n{new_map:#?}",
102                );
103
104                RestError::new(StatusCode::CONFLICT, message)
105            }
106            TimeoutBeforeFinality | FailedWithTransientErrorAfterMaximumAttempts { .. } => {
107                // TODO add a Retry-After header
108                RestError::new(
109                    StatusCode::SERVICE_UNAVAILABLE,
110                    "timed-out before finality could be reached",
111                )
112            }
113            NonRecoverableTransactionError { errors } => {
114                let new_errors: Vec<String> = errors
115                    .into_iter()
116                    // sort by total stake, descending, so users see the most prominent one first
117                    .sorted_by(|(_, a, _), (_, b, _)| b.cmp(a))
118                    .filter_map(|(err, _, _)| {
119                        match &err {
120                            // Special handling of UserInputError:
121                            // ObjectNotFound and DependentPackageNotFound are considered
122                            // retryable errors but they have different treatment
123                            // in AuthorityAggregator.
124                            // The optimal fix would be to examine if the total stake
125                            // of ObjectNotFound/DependentPackageNotFound exceeds the
126                            // quorum threshold, but it takes a Committee here.
127                            // So, we take an easier route and consider them non-retryable
128                            // at all. Combining this with the sorting above, clients will
129                            // see the dominant error first.
130                            IotaError::UserInput { error } => Some(error.to_string()),
131                            _ => {
132                                if err.is_retryable().0 {
133                                    None
134                                } else {
135                                    Some(err.to_string())
136                                }
137                            }
138                        }
139                    })
140                    .collect();
141
142                assert!(
143                    !new_errors.is_empty(),
144                    "NonRecoverableTransactionError should have at least one non-retryable error"
145                );
146
147                let error_list = new_errors.join(", ");
148                let error_msg = format!(
149                    "Transaction execution failed due to issues with transaction inputs, please review the errors and try again: {}.",
150                    error_list
151                );
152
153                RestError::new(StatusCode::BAD_REQUEST, error_msg)
154            }
155            TxAlreadyFinalizedWithDifferentUserSignatures => RestError::new(
156                StatusCode::CONFLICT,
157                "The transaction is already finalized but with different user signatures",
158            ),
159            SystemOverload { .. } | SystemOverloadRetryAfter { .. } => {
160                // TODO add a Retry-After header
161                RestError::new(StatusCode::SERVICE_UNAVAILABLE, "system is overloaded")
162            }
163        }
164    }
165}