casper_json_rpc/
error.rs

1use std::{borrow::Cow, fmt::Debug, hash::Hash};
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use tracing::{error, warn};
6
7/// A marker trait for a type suitable for use as an error code when constructing an [`Error`].
8///
9/// The implementing type must also implement `Into<(i64, &'static str)>` where the tuple represents
10/// the "code" and "message" fields of the `Error`.
11///
12/// As per the JSON-RPC specification, the code must not fall in the reserved range, i.e. it must
13/// not be between -32768 and -32000 inclusive.
14///
15/// Generally the "message" will be a brief const &str, where additional request-specific info can
16/// be provided via the `additional_info` parameter of [`Error::new`].
17///
18/// # Example
19///
20/// ```
21/// use serde::{Deserialize, Serialize};
22/// use casper_json_rpc::ErrorCodeT;
23///
24/// #[derive(Copy, Clone, Eq, PartialEq, Serialize, Deserialize, Debug)]
25/// #[repr(i64)]
26/// pub enum ErrorCode {
27///     /// The requested item was not found.
28///     NoSuchItem = -1,
29///     /// Failed to put the requested item to storage.
30///     FailedToPutItem = -2,
31/// }
32///
33/// impl From<ErrorCode> for (i64, &'static str) {
34///     fn from(error_code: ErrorCode) -> Self {
35///         match error_code {
36///             ErrorCode::NoSuchItem => (error_code as i64, "No such item"),
37///             ErrorCode::FailedToPutItem => (error_code as i64, "Failed to put item"),
38///         }
39///     }
40/// }
41///
42/// impl ErrorCodeT for ErrorCode {}
43/// ```
44pub trait ErrorCodeT:
45    Into<(i64, &'static str)> + for<'de> Deserialize<'de> + Copy + Eq + Debug
46{
47    /// Whether this type represents reserved error codes or not.
48    ///
49    /// This should normally be left with the default return value of `false`.
50    #[doc(hidden)]
51    fn is_reserved() -> bool {
52        false
53    }
54}
55
56/// The various reserved codes which can be returned in the JSON-RPC response's "error" object.
57///
58/// See [the JSON-RPC Specification](https://www.jsonrpc.org/specification#error_object) for further
59/// details.
60#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, Debug)]
61#[repr(i64)]
62pub enum ReservedErrorCode {
63    /// Invalid JSON was received by the server.
64    ParseError = -32700,
65    /// The JSON sent is not a valid Request object.
66    InvalidRequest = -32600,
67    /// The method does not exist or is not available.
68    MethodNotFound = -32601,
69    /// Invalid method parameter(s).
70    InvalidParams = -32602,
71    /// Internal JSON-RPC error.
72    InternalError = -32603,
73}
74
75impl From<ReservedErrorCode> for (i64, &'static str) {
76    fn from(error_code: ReservedErrorCode) -> Self {
77        match error_code {
78            ReservedErrorCode::ParseError => (error_code as i64, "Parse error"),
79            ReservedErrorCode::InvalidRequest => (error_code as i64, "Invalid Request"),
80            ReservedErrorCode::MethodNotFound => (error_code as i64, "Method not found"),
81            ReservedErrorCode::InvalidParams => (error_code as i64, "Invalid params"),
82            ReservedErrorCode::InternalError => (error_code as i64, "Internal error"),
83        }
84    }
85}
86
87impl ErrorCodeT for ReservedErrorCode {
88    fn is_reserved() -> bool {
89        true
90    }
91}
92
93/// An object suitable to be returned in a JSON-RPC response as the "error" field.
94///
95/// See [the JSON-RPC Specification](https://www.jsonrpc.org/specification#error_object) for further
96/// details.
97#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Debug)]
98#[serde(deny_unknown_fields)]
99pub struct Error {
100    /// A number that indicates the error type that occurred.
101    code: i64,
102    /// A short description of the error.
103    message: Cow<'static, str>,
104    /// Additional information about the error.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    data: Option<Value>,
107}
108
109impl Error {
110    /// Returns a new `Error`, converting `error_code` to the "code" and "message" fields, and
111    /// JSON-encoding `additional_info` as the "data" field.
112    ///
113    /// Other than when providing a [`ReservedErrorCode`], the converted "code" must not fall in the
114    /// reserved range as defined in the JSON-RPC specification, i.e. it must not be between -32768
115    /// and -32100 inclusive.
116    ///
117    /// Note that in an upcoming release, the restriction will be tightened to disallow error codes
118    /// in the implementation-defined server-errors range.  I.e. codes in the range -32768 to -32000
119    /// inclusive will be disallowed.
120    ///
121    /// If the converted code is within the reserved range when it should not be, or if
122    /// JSON-encoding `additional_data` fails, the returned `Self` is built from
123    /// [`ReservedErrorCode::InternalError`] with the "data" field being a String providing more
124    /// info on the underlying error.
125    pub fn new<C: ErrorCodeT, T: Serialize>(error_code: C, additional_info: T) -> Self {
126        let (code, message): (i64, &'static str) = error_code.into();
127
128        if !C::is_reserved() && (-32768..=-32100).contains(&code) {
129            warn!(%code, "provided json-rpc error code is reserved; returning internal error");
130            let (code, message) = ReservedErrorCode::InternalError.into();
131            return Error {
132                code,
133                message: Cow::Borrowed(message),
134                data: Some(Value::String(format!(
135                    "attempted to return reserved error code {}",
136                    code
137                ))),
138            };
139        }
140
141        let data = match serde_json::to_value(additional_info) {
142            Ok(Value::Null) => None,
143            Ok(value) => Some(value),
144            Err(error) => {
145                error!(%error, "failed to json-encode additional info in json-rpc error");
146                let (code, message) = ReservedErrorCode::InternalError.into();
147                return Error {
148                    code,
149                    message: Cow::Borrowed(message),
150                    data: Some(Value::String(format!(
151                        "failed to json-encode additional info in json-rpc error: {}",
152                        error
153                    ))),
154                };
155            }
156        };
157
158        Error {
159            code,
160            message: Cow::Borrowed(message),
161            data,
162        }
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use serde::ser::{Error as _, Serializer};
169
170    use super::*;
171
172    #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, Debug)]
173    struct TestErrorCode {
174        // If `true` the error code will be one in the reserved range.
175        in_reserved_range: bool,
176    }
177
178    impl From<TestErrorCode> for (i64, &'static str) {
179        fn from(error_code: TestErrorCode) -> Self {
180            if error_code.in_reserved_range {
181                (-32768, "Invalid test error")
182            } else {
183                (-123, "Valid test error")
184            }
185        }
186    }
187
188    impl ErrorCodeT for TestErrorCode {}
189
190    #[derive(Serialize)]
191    struct AdditionalInfo {
192        id: u64,
193        context: &'static str,
194    }
195
196    impl Default for AdditionalInfo {
197        fn default() -> Self {
198            AdditionalInfo {
199                id: 1314,
200                context: "TEST",
201            }
202        }
203    }
204
205    struct FailToEncode;
206
207    impl Serialize for FailToEncode {
208        fn serialize<S: Serializer>(&self, _serializer: S) -> Result<S::Ok, S::Error> {
209            Err(S::Error::custom("won't encode"))
210        }
211    }
212
213    #[test]
214    fn should_construct_reserved_error() {
215        const EXPECTED_WITH_DATA: &str =
216            r#"{"code":-32700,"message":"Parse error","data":{"id":1314,"context":"TEST"}}"#;
217        const EXPECTED_WITHOUT_DATA: &str = r#"{"code":-32601,"message":"Method not found"}"#;
218        const EXPECTED_WITH_BAD_DATA: &str = r#"{"code":-32603,"message":"Internal error","data":"failed to json-encode additional info in json-rpc error: won't encode"}"#;
219
220        let error_with_data = Error::new(ReservedErrorCode::ParseError, AdditionalInfo::default());
221        let encoded = serde_json::to_string(&error_with_data).unwrap();
222        assert_eq!(encoded, EXPECTED_WITH_DATA);
223
224        let error_without_data = Error::new(ReservedErrorCode::MethodNotFound, None::<u8>);
225        let encoded = serde_json::to_string(&error_without_data).unwrap();
226        assert_eq!(encoded, EXPECTED_WITHOUT_DATA);
227
228        let error_with_bad_data = Error::new(ReservedErrorCode::InvalidParams, FailToEncode);
229        let encoded = serde_json::to_string(&error_with_bad_data).unwrap();
230        assert_eq!(encoded, EXPECTED_WITH_BAD_DATA);
231    }
232
233    #[test]
234    fn should_construct_custom_error() {
235        const EXPECTED_WITH_DATA: &str =
236            r#"{"code":-123,"message":"Valid test error","data":{"id":1314,"context":"TEST"}}"#;
237        const EXPECTED_WITHOUT_DATA: &str = r#"{"code":-123,"message":"Valid test error"}"#;
238        const EXPECTED_WITH_BAD_DATA: &str = r#"{"code":-32603,"message":"Internal error","data":"failed to json-encode additional info in json-rpc error: won't encode"}"#;
239
240        let good_error_code = TestErrorCode {
241            in_reserved_range: false,
242        };
243
244        let error_with_data = Error::new(good_error_code, AdditionalInfo::default());
245        let encoded = serde_json::to_string(&error_with_data).unwrap();
246        assert_eq!(encoded, EXPECTED_WITH_DATA);
247
248        let error_without_data = Error::new(good_error_code, ());
249        let encoded = serde_json::to_string(&error_without_data).unwrap();
250        assert_eq!(encoded, EXPECTED_WITHOUT_DATA);
251
252        let error_with_bad_data = Error::new(good_error_code, FailToEncode);
253        let encoded = serde_json::to_string(&error_with_bad_data).unwrap();
254        assert_eq!(encoded, EXPECTED_WITH_BAD_DATA);
255    }
256
257    #[test]
258    fn should_fall_back_to_internal_error_on_bad_custom_error() {
259        const EXPECTED: &str = r#"{"code":-32603,"message":"Internal error","data":"attempted to return reserved error code -32603"}"#;
260
261        let bad_error_code = TestErrorCode {
262            in_reserved_range: true,
263        };
264
265        let error_with_data = Error::new(bad_error_code, AdditionalInfo::default());
266        let encoded = serde_json::to_string(&error_with_data).unwrap();
267        assert_eq!(encoded, EXPECTED);
268
269        let error_without_data = Error::new(bad_error_code, None::<u8>);
270        let encoded = serde_json::to_string(&error_without_data).unwrap();
271        assert_eq!(encoded, EXPECTED);
272
273        let error_with_bad_data = Error::new(bad_error_code, FailToEncode);
274        let encoded = serde_json::to_string(&error_with_bad_data).unwrap();
275        assert_eq!(encoded, EXPECTED);
276    }
277}