use std::{borrow::Cow, fmt::Debug, hash::Hash};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tracing::{error, warn};
pub trait ErrorCodeT:
Into<(i64, &'static str)> + for<'de> Deserialize<'de> + Copy + Eq + Debug
{
#[doc(hidden)]
fn is_reserved() -> bool {
false
}
}
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, Debug)]
#[repr(i64)]
pub enum ReservedErrorCode {
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
}
impl From<ReservedErrorCode> for (i64, &'static str) {
fn from(error_code: ReservedErrorCode) -> Self {
match error_code {
ReservedErrorCode::ParseError => (error_code as i64, "Parse error"),
ReservedErrorCode::InvalidRequest => (error_code as i64, "Invalid Request"),
ReservedErrorCode::MethodNotFound => (error_code as i64, "Method not found"),
ReservedErrorCode::InvalidParams => (error_code as i64, "Invalid params"),
ReservedErrorCode::InternalError => (error_code as i64, "Internal error"),
}
}
}
impl ErrorCodeT for ReservedErrorCode {
fn is_reserved() -> bool {
true
}
}
#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct Error {
code: i64,
message: Cow<'static, str>,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Value>,
}
impl Error {
pub fn new<C: ErrorCodeT, T: Serialize>(error_code: C, additional_info: T) -> Self {
let (code, message): (i64, &'static str) = error_code.into();
if !C::is_reserved() && (-32768..=-32100).contains(&code) {
warn!(%code, "provided json-rpc error code is reserved; returning internal error");
let (code, message) = ReservedErrorCode::InternalError.into();
return Error {
code,
message: Cow::Borrowed(message),
data: Some(Value::String(format!(
"attempted to return reserved error code {}",
code
))),
};
}
let data = match serde_json::to_value(additional_info) {
Ok(Value::Null) => None,
Ok(value) => Some(value),
Err(error) => {
error!(%error, "failed to json-encode additional info in json-rpc error");
let (code, message) = ReservedErrorCode::InternalError.into();
return Error {
code,
message: Cow::Borrowed(message),
data: Some(Value::String(format!(
"failed to json-encode additional info in json-rpc error: {}",
error
))),
};
}
};
Error {
code,
message: Cow::Borrowed(message),
data,
}
}
}
#[cfg(test)]
mod tests {
use serde::ser::{Error as _, Serializer};
use super::*;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, Debug)]
struct TestErrorCode {
in_reserved_range: bool,
}
impl From<TestErrorCode> for (i64, &'static str) {
fn from(error_code: TestErrorCode) -> Self {
if error_code.in_reserved_range {
(-32768, "Invalid test error")
} else {
(-123, "Valid test error")
}
}
}
impl ErrorCodeT for TestErrorCode {}
#[derive(Serialize)]
struct AdditionalInfo {
id: u64,
context: &'static str,
}
impl Default for AdditionalInfo {
fn default() -> Self {
AdditionalInfo {
id: 1314,
context: "TEST",
}
}
}
struct FailToEncode;
impl Serialize for FailToEncode {
fn serialize<S: Serializer>(&self, _serializer: S) -> Result<S::Ok, S::Error> {
Err(S::Error::custom("won't encode"))
}
}
#[test]
fn should_construct_reserved_error() {
const EXPECTED_WITH_DATA: &str =
r#"{"code":-32700,"message":"Parse error","data":{"id":1314,"context":"TEST"}}"#;
const EXPECTED_WITHOUT_DATA: &str = r#"{"code":-32601,"message":"Method not found"}"#;
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"}"#;
let error_with_data = Error::new(ReservedErrorCode::ParseError, AdditionalInfo::default());
let encoded = serde_json::to_string(&error_with_data).unwrap();
assert_eq!(encoded, EXPECTED_WITH_DATA);
let error_without_data = Error::new(ReservedErrorCode::MethodNotFound, None::<u8>);
let encoded = serde_json::to_string(&error_without_data).unwrap();
assert_eq!(encoded, EXPECTED_WITHOUT_DATA);
let error_with_bad_data = Error::new(ReservedErrorCode::InvalidParams, FailToEncode);
let encoded = serde_json::to_string(&error_with_bad_data).unwrap();
assert_eq!(encoded, EXPECTED_WITH_BAD_DATA);
}
#[test]
fn should_construct_custom_error() {
const EXPECTED_WITH_DATA: &str =
r#"{"code":-123,"message":"Valid test error","data":{"id":1314,"context":"TEST"}}"#;
const EXPECTED_WITHOUT_DATA: &str = r#"{"code":-123,"message":"Valid test error"}"#;
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"}"#;
let good_error_code = TestErrorCode {
in_reserved_range: false,
};
let error_with_data = Error::new(good_error_code, AdditionalInfo::default());
let encoded = serde_json::to_string(&error_with_data).unwrap();
assert_eq!(encoded, EXPECTED_WITH_DATA);
let error_without_data = Error::new(good_error_code, ());
let encoded = serde_json::to_string(&error_without_data).unwrap();
assert_eq!(encoded, EXPECTED_WITHOUT_DATA);
let error_with_bad_data = Error::new(good_error_code, FailToEncode);
let encoded = serde_json::to_string(&error_with_bad_data).unwrap();
assert_eq!(encoded, EXPECTED_WITH_BAD_DATA);
}
#[test]
fn should_fall_back_to_internal_error_on_bad_custom_error() {
const EXPECTED: &str = r#"{"code":-32603,"message":"Internal error","data":"attempted to return reserved error code -32603"}"#;
let bad_error_code = TestErrorCode {
in_reserved_range: true,
};
let error_with_data = Error::new(bad_error_code, AdditionalInfo::default());
let encoded = serde_json::to_string(&error_with_data).unwrap();
assert_eq!(encoded, EXPECTED);
let error_without_data = Error::new(bad_error_code, None::<u8>);
let encoded = serde_json::to_string(&error_without_data).unwrap();
assert_eq!(encoded, EXPECTED);
let error_with_bad_data = Error::new(bad_error_code, FailToEncode);
let encoded = serde_json::to_string(&error_with_bad_data).unwrap();
assert_eq!(encoded, EXPECTED);
}
}