use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
#[repr(i32)]
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq, Hash)]
pub enum ErrorCode {
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
ServerError = -32000,
ResourceUnavailable = -32004,
RootNotAnchored = -32005,
PeerUnreachable = -32006,
RangeNotSatisfiable = -32007,
ContentRedirect = -32008,
UpstreamError = -32010,
StageInvalidInput = -32011,
StageNoFiles = -32012,
StageOverCap = -32013,
StageCompileFailed = -32014,
OnionCircuitUnavailable = -32020,
PrivacyRequiresLocalNode = -32021,
OnionHopsOutOfRange = -32022,
Unauthorized = -32030,
NotSupported = -32031,
ControlError = -32032,
}
impl ErrorCode {
pub const fn code(self) -> i32 {
self as i32
}
pub const fn machine_code(self) -> &'static str {
match self {
ErrorCode::ParseError => "PARSE_ERROR",
ErrorCode::InvalidRequest => "INVALID_REQUEST",
ErrorCode::MethodNotFound => "METHOD_NOT_FOUND",
ErrorCode::InvalidParams => "INVALID_PARAMS",
ErrorCode::InternalError => "INTERNAL_ERROR",
ErrorCode::ServerError => "SERVER_ERROR",
ErrorCode::ResourceUnavailable => "RESOURCE_UNAVAILABLE",
ErrorCode::RootNotAnchored => "ROOT_NOT_ANCHORED",
ErrorCode::PeerUnreachable => "PEER_UNREACHABLE",
ErrorCode::RangeNotSatisfiable => "RANGE_NOT_SATISFIABLE",
ErrorCode::ContentRedirect => "CONTENT_REDIRECT",
ErrorCode::UpstreamError => "UPSTREAM_ERROR",
ErrorCode::StageInvalidInput => "STAGE_INVALID_INPUT",
ErrorCode::StageNoFiles => "STAGE_NO_FILES",
ErrorCode::StageOverCap => "STAGE_OVER_CAP",
ErrorCode::StageCompileFailed => "STAGE_COMPILE_FAILED",
ErrorCode::OnionCircuitUnavailable => "ONION_CIRCUIT_UNAVAILABLE",
ErrorCode::PrivacyRequiresLocalNode => "PRIVACY_REQUIRES_LOCAL_NODE",
ErrorCode::OnionHopsOutOfRange => "ONION_HOPS_OUT_OF_RANGE",
ErrorCode::Unauthorized => "UNAUTHORIZED",
ErrorCode::NotSupported => "NOT_SUPPORTED",
ErrorCode::ControlError => "CONTROL_ERROR",
}
}
pub const fn default_message(self) -> &'static str {
match self {
ErrorCode::ParseError => "Parse error",
ErrorCode::InvalidRequest => "Invalid request",
ErrorCode::MethodNotFound => "Method not found",
ErrorCode::InvalidParams => "Invalid params",
ErrorCode::InternalError => "Internal error",
ErrorCode::ServerError => "Server error",
ErrorCode::ResourceUnavailable => "Resource not available at the requested root",
ErrorCode::RootNotAnchored => "Root not chain-anchored",
ErrorCode::PeerUnreachable => "Peer unreachable",
ErrorCode::RangeNotSatisfiable => "Range not satisfiable",
ErrorCode::ContentRedirect => "Content held elsewhere — redirect",
ErrorCode::UpstreamError => "Upstream error",
ErrorCode::StageInvalidInput => "Stage input directory not readable",
ErrorCode::StageNoFiles => "Stage input contained no files",
ErrorCode::StageOverCap => "Stage input over the store cap",
ErrorCode::StageCompileFailed => "Stage compile failed",
ErrorCode::OnionCircuitUnavailable => "Onion circuit unavailable",
ErrorCode::PrivacyRequiresLocalNode => "Privacy requires a local node",
ErrorCode::OnionHopsOutOfRange => "Onion hop count out of range",
ErrorCode::Unauthorized => "Unauthorized",
ErrorCode::NotSupported => "Not supported",
ErrorCode::ControlError => "Control-plane error",
}
}
pub const fn default_origin(self) -> ErrorOrigin {
match self {
ErrorCode::UpstreamError => ErrorOrigin::Upstream,
ErrorCode::OnionCircuitUnavailable
| ErrorCode::PrivacyRequiresLocalNode
| ErrorCode::OnionHopsOutOfRange => ErrorOrigin::Onion,
ErrorCode::Unauthorized | ErrorCode::NotSupported | ErrorCode::ControlError => {
ErrorOrigin::Control
}
ErrorCode::PeerUnreachable => ErrorOrigin::Peer,
_ => ErrorOrigin::Node,
}
}
pub const fn is_jsonrpc_reserved(self) -> bool {
let c = self.code();
c >= -32768 && c <= -32000
}
pub const ALL: &'static [ErrorCode] = &[
ErrorCode::ParseError,
ErrorCode::InvalidRequest,
ErrorCode::MethodNotFound,
ErrorCode::InvalidParams,
ErrorCode::InternalError,
ErrorCode::ServerError,
ErrorCode::ResourceUnavailable,
ErrorCode::RootNotAnchored,
ErrorCode::PeerUnreachable,
ErrorCode::RangeNotSatisfiable,
ErrorCode::ContentRedirect,
ErrorCode::UpstreamError,
ErrorCode::StageInvalidInput,
ErrorCode::StageNoFiles,
ErrorCode::StageOverCap,
ErrorCode::StageCompileFailed,
ErrorCode::OnionCircuitUnavailable,
ErrorCode::PrivacyRequiresLocalNode,
ErrorCode::OnionHopsOutOfRange,
ErrorCode::Unauthorized,
ErrorCode::NotSupported,
ErrorCode::ControlError,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ErrorOrigin {
Node,
Peer,
Upstream,
Onion,
Control,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ErrorData {
pub code: String,
pub origin: ErrorOrigin,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub redirect: Option<crate::types::RedirectInfo>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RpcError {
pub code: ErrorCode,
pub message: String,
pub data: ErrorData,
}
impl RpcError {
pub fn new(code: ErrorCode, message: impl Into<String>, origin: ErrorOrigin) -> Self {
Self {
code,
message: message.into(),
data: ErrorData {
code: code.machine_code().to_string(),
origin,
redirect: None,
extra: serde_json::Map::new(),
},
}
}
pub fn of(code: ErrorCode, message: impl Into<String>) -> Self {
Self::new(code, message, code.default_origin())
}
pub fn code_only(code: ErrorCode) -> Self {
Self::new(code, code.default_message(), code.default_origin())
}
pub fn with_redirect(mut self, redirect: crate::types::RedirectInfo) -> Self {
self.data.redirect = Some(redirect);
self
}
pub fn with_extra(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.data.extra.insert(key.into(), value);
self
}
}
impl std::fmt::Display for RpcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.code.machine_code(), self.message)
}
}
impl std::error::Error for RpcError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn numeric_values_pinned() {
assert_eq!(ErrorCode::ParseError.code(), -32700);
assert_eq!(ErrorCode::InvalidRequest.code(), -32600);
assert_eq!(ErrorCode::MethodNotFound.code(), -32601);
assert_eq!(ErrorCode::InvalidParams.code(), -32602);
assert_eq!(ErrorCode::InternalError.code(), -32603);
assert_eq!(ErrorCode::ServerError.code(), -32000);
assert_eq!(ErrorCode::ResourceUnavailable.code(), -32004);
assert_eq!(ErrorCode::RootNotAnchored.code(), -32005);
assert_eq!(ErrorCode::PeerUnreachable.code(), -32006);
assert_eq!(ErrorCode::RangeNotSatisfiable.code(), -32007);
assert_eq!(ErrorCode::ContentRedirect.code(), -32008);
assert_eq!(ErrorCode::UpstreamError.code(), -32010);
assert_eq!(ErrorCode::StageInvalidInput.code(), -32011);
assert_eq!(ErrorCode::StageNoFiles.code(), -32012);
assert_eq!(ErrorCode::StageOverCap.code(), -32013);
assert_eq!(ErrorCode::StageCompileFailed.code(), -32014);
assert_eq!(ErrorCode::OnionCircuitUnavailable.code(), -32020);
assert_eq!(ErrorCode::PrivacyRequiresLocalNode.code(), -32021);
assert_eq!(ErrorCode::OnionHopsOutOfRange.code(), -32022);
assert_eq!(ErrorCode::Unauthorized.code(), -32030);
assert_eq!(ErrorCode::NotSupported.code(), -32031);
assert_eq!(ErrorCode::ControlError.code(), -32032);
}
#[test]
fn onion_control_collision_resolved() {
assert_eq!(ErrorCode::OnionCircuitUnavailable.code(), -32020);
assert_eq!(ErrorCode::Unauthorized.code(), -32030);
assert_ne!(
ErrorCode::OnionCircuitUnavailable.code(),
ErrorCode::Unauthorized.code()
);
}
#[test]
fn code_serialises_as_integer() {
assert_eq!(
serde_json::to_string(&ErrorCode::MethodNotFound).unwrap(),
"-32601"
);
assert_eq!(
serde_json::to_string(&ErrorCode::ContentRedirect).unwrap(),
"-32008"
);
}
#[test]
fn all_codes_unique_and_complete() {
use std::collections::HashSet;
let nums: HashSet<i32> = ErrorCode::ALL.iter().map(|c| c.code()).collect();
let strs: HashSet<&str> = ErrorCode::ALL.iter().map(|c| c.machine_code()).collect();
assert_eq!(nums.len(), ErrorCode::ALL.len(), "duplicate numeric code");
assert_eq!(strs.len(), ErrorCode::ALL.len(), "duplicate machine code");
assert_eq!(ErrorCode::ALL.len(), 22);
}
#[test]
fn envelope_shape_and_data_code() {
let e = RpcError::new(
ErrorCode::ResourceUnavailable,
"not here",
ErrorOrigin::Node,
);
let v = serde_json::to_value(&e).unwrap();
assert_eq!(v["code"], -32004);
assert_eq!(v["message"], "not here");
assert_eq!(v["data"]["code"], "RESOURCE_UNAVAILABLE");
assert_eq!(v["data"]["origin"], "node");
let back: RpcError = serde_json::from_value(v).unwrap();
assert_eq!(back, e);
}
#[test]
fn upstream_default_origin() {
let e = RpcError::of(ErrorCode::UpstreamError, "upstream: boom");
assert_eq!(e.data.origin, ErrorOrigin::Upstream);
assert_eq!(
serde_json::to_value(&e).unwrap()["data"]["origin"],
"upstream"
);
}
#[test]
fn code_only_defaults() {
let e = RpcError::code_only(ErrorCode::PeerUnreachable);
assert_eq!(e.message, "Peer unreachable");
assert_eq!(e.data.origin, ErrorOrigin::Peer);
}
}