Skip to main content

rightkit_daemon/
error.rs

1use crate::host_identity::HostIdentityError;
2use serde_json::{json, Value};
3use std::fmt;
4
5/// The host is handing the service to a higher-ranked host. Retryable: re-read
6/// discovery and resubmit the whole request.
7pub const ERROR_HOST_HANDOFF: &str = "host_handoff";
8/// The process behind the endpoint is not the host the caller expected
9/// ([`Client::connect_expecting`](crate::Client::connect_expecting)). Not retryable: reconnecting
10/// reaches the same impostor, and no application byte was sent to it.
11pub const ERROR_HOST_IDENTITY: &str = "host_identity";
12/// The suite root could not be resolved, or a [`ClientConfig`](crate::ClientConfig) that ignores
13/// environment overrides was handed a suite that came from the environment. Not retryable.
14pub const ERROR_SUITE_UNRESOLVED: &str = "suite_unresolved";
15/// The calling app may not call this method (or is not on the allowlist).
16pub const ERROR_NOT_ALLOWED: &str = "request_not_allowed";
17/// The handler's result did not fit the host's frame limit, so it was not sent. Not retryable:
18/// the same request produces the same result. Ask for less (a page, a smaller body).
19pub const ERROR_RESPONSE_TOO_LARGE: &str = "response_too_large";
20/// The connection dropped before a response arrived. Retryable.
21pub const ERROR_CONNECTION_CLOSED: &str = "connection_closed";
22/// The per-request deadline (connect + write + read) expired and the
23/// connection was aborted. Not retryable: the request may already have run on
24/// the host, so it is never replayed automatically.
25pub const ERROR_TIMEOUT: &str = "request_timeout";
26/// No live host for the service. Retryable.
27pub const ERROR_UNAVAILABLE: &str = "service_unavailable";
28
29/// The caller presented no valid credential (a device that is unknown, or whose signature failed).
30pub const ERROR_UNAUTHENTICATED: &str = "unauthenticated";
31/// The caller is known but lacks the scope for this method.
32pub const ERROR_FORBIDDEN: &str = "forbidden";
33/// A write named a stale revision.
34pub const ERROR_REVISION_CONFLICT: &str = "revision_conflict";
35
36/// Typed wire error. `retryable` means resubmitting the request can succeed.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ServiceError {
39    pub code: String,
40    pub message: String,
41    pub retryable: bool,
42}
43
44impl ServiceError {
45    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
46        Self {
47            code: code.into(),
48            message: message.into(),
49            retryable: false,
50        }
51    }
52    pub fn retryable(code: impl Into<String>, message: impl Into<String>) -> Self {
53        Self {
54            code: code.into(),
55            message: message.into(),
56            retryable: true,
57        }
58    }
59    pub fn handoff() -> Self {
60        Self::retryable(
61            ERROR_HOST_HANDOFF,
62            "host is handing the service to a higher-ranked host; retry",
63        )
64    }
65    pub fn to_value(&self) -> Value {
66        json!({"code": self.code, "message": self.message, "retryable": self.retryable})
67    }
68    pub fn from_value(v: &Value) -> Self {
69        Self {
70            code: v["code"].as_str().unwrap_or("error").into(),
71            message: v["message"].as_str().unwrap_or("").into(),
72            retryable: v["retryable"].as_bool().unwrap_or(false),
73        }
74    }
75}
76
77impl fmt::Display for ServiceError {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "{}: {}", self.code, self.message)
80    }
81}
82impl std::error::Error for ServiceError {}
83impl From<std::io::Error> for ServiceError {
84    fn from(e: std::io::Error) -> Self {
85        Self::new("io", e.to_string())
86    }
87}
88impl From<HostIdentityError> for ServiceError {
89    /// The wire-style form of a host identity refusal: code [`ERROR_HOST_IDENTITY`], not retryable.
90    fn from(e: HostIdentityError) -> Self {
91        Self::new(ERROR_HOST_IDENTITY, e.to_string())
92    }
93}
94
95/// Why [`Client::connect_expecting`](crate::Client::connect_expecting) did not return a client.
96///
97/// The two variants are different facts. [`ConnectError::Service`] is the ordinary connection
98/// story (no record, host unreachable, protocol mismatch, hello refused, timeout) and keeps its
99/// `retryable` flag. [`ConnectError::HostIdentity`] means something answered at the endpoint and
100/// the OS says it is not the host the caller named; no application byte was sent to it.
101#[derive(Debug, Clone, PartialEq, Eq)]
102#[non_exhaustive]
103pub enum ConnectError {
104    Service(ServiceError),
105    HostIdentity(HostIdentityError),
106}
107
108impl ConnectError {
109    /// Collapse into the single wire-style error. A host identity refusal becomes code
110    /// [`ERROR_HOST_IDENTITY`], not retryable, with the typed detail in the message.
111    pub fn into_service_error(self) -> ServiceError {
112        match self {
113            ConnectError::Service(e) => e,
114            ConnectError::HostIdentity(e) => e.into(),
115        }
116    }
117}
118
119impl fmt::Display for ConnectError {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        match self {
122            ConnectError::Service(e) => fmt::Display::fmt(e, f),
123            ConnectError::HostIdentity(e) => fmt::Display::fmt(e, f),
124        }
125    }
126}
127
128impl std::error::Error for ConnectError {
129    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
130        match self {
131            ConnectError::Service(e) => Some(e),
132            ConnectError::HostIdentity(e) => Some(e),
133        }
134    }
135}
136
137impl From<ServiceError> for ConnectError {
138    fn from(e: ServiceError) -> Self {
139        ConnectError::Service(e)
140    }
141}
142
143impl From<HostIdentityError> for ConnectError {
144    fn from(e: HostIdentityError) -> Self {
145        ConnectError::HostIdentity(e)
146    }
147}