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