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/// A request is larger than the host will assemble from chunks
21/// ([`HostOptions::max_message`](crate::HostOptions::max_message)), or than one frame when the
22/// host does not chunk. Not retryable. Ask for less.
23pub const ERROR_REQUEST_TOO_LARGE: &str = "request_too_large";
24/// A chunk frame (`rk.upload`, `rk.fetch`) was malformed, out of order, repeated or for an upload
25/// or result that is gone. Not retryable as sent: start the whole transfer again.
26pub const ERROR_BAD_CHUNK: &str = "bad_chunk";
27/// The connection dropped before a response arrived. Retryable.
28pub const ERROR_CONNECTION_CLOSED: &str = "connection_closed";
29/// The per-request deadline (connect + write + read) expired and the
30/// connection was aborted. Not retryable: the request may already have run on
31/// the host, so it is never replayed automatically.
32pub const ERROR_TIMEOUT: &str = "request_timeout";
33/// No live host for the service. Retryable.
34pub const ERROR_UNAVAILABLE: &str = "service_unavailable";
35
36/// The caller presented no valid credential (a device that is unknown, or whose signature failed).
37pub const ERROR_UNAUTHENTICATED: &str = "unauthenticated";
38/// The caller is known but lacks the scope for this method.
39pub const ERROR_FORBIDDEN: &str = "forbidden";
40/// A write named a stale revision.
41pub const ERROR_REVISION_CONFLICT: &str = "revision_conflict";
42/// Too many failed authentications from this connection, or from all callers together, inside
43/// the failure window ([`crate::fail_limit`]). Not retryable on the same connection; the block
44/// lapses on its own, so wait before reconnecting.
45pub const ERROR_RATE_LIMITED: &str = "rate_limited";
46
47/// Typed wire error. `retryable` means resubmitting the request can succeed.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct ServiceError {
50    pub code: String,
51    pub message: String,
52    pub retryable: bool,
53}
54
55impl ServiceError {
56    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
57        Self {
58            code: code.into(),
59            message: message.into(),
60            retryable: false,
61        }
62    }
63    pub fn retryable(code: impl Into<String>, message: impl Into<String>) -> Self {
64        Self {
65            code: code.into(),
66            message: message.into(),
67            retryable: true,
68        }
69    }
70    pub fn handoff() -> Self {
71        Self::retryable(
72            ERROR_HOST_HANDOFF,
73            "host is handing the service to a higher-ranked host; retry",
74        )
75    }
76    pub fn to_value(&self) -> Value {
77        json!({"code": self.code, "message": self.message, "retryable": self.retryable})
78    }
79    pub fn from_value(v: &Value) -> Self {
80        Self {
81            code: v["code"].as_str().unwrap_or("error").into(),
82            message: v["message"].as_str().unwrap_or("").into(),
83            retryable: v["retryable"].as_bool().unwrap_or(false),
84        }
85    }
86}
87
88impl fmt::Display for ServiceError {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        write!(f, "{}: {}", self.code, self.message)
91    }
92}
93impl std::error::Error for ServiceError {}
94impl From<std::io::Error> for ServiceError {
95    fn from(e: std::io::Error) -> Self {
96        Self::new("io", e.to_string())
97    }
98}
99impl From<HostIdentityError> for ServiceError {
100    /// The wire-style form of a host identity refusal: code [`ERROR_HOST_IDENTITY`], not retryable.
101    fn from(e: HostIdentityError) -> Self {
102        Self::new(ERROR_HOST_IDENTITY, e.to_string())
103    }
104}
105
106/// Why [`Client::connect_expecting`](crate::Client::connect_expecting) did not return a client.
107///
108/// The two variants are different facts. [`ConnectError::Service`] is the ordinary connection
109/// story (no record, host unreachable, protocol mismatch, hello refused, timeout) and keeps its
110/// `retryable` flag. [`ConnectError::HostIdentity`] means something answered at the endpoint and
111/// the OS says it is not the host the caller named; no application byte was sent to it.
112#[derive(Debug, Clone, PartialEq, Eq)]
113#[non_exhaustive]
114pub enum ConnectError {
115    Service(ServiceError),
116    HostIdentity(HostIdentityError),
117}
118
119impl ConnectError {
120    /// Collapse into the single wire-style error. A host identity refusal becomes code
121    /// [`ERROR_HOST_IDENTITY`], not retryable, with the typed detail in the message.
122    pub fn into_service_error(self) -> ServiceError {
123        match self {
124            ConnectError::Service(e) => e,
125            ConnectError::HostIdentity(e) => e.into(),
126        }
127    }
128}
129
130impl fmt::Display for ConnectError {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        match self {
133            ConnectError::Service(e) => fmt::Display::fmt(e, f),
134            ConnectError::HostIdentity(e) => fmt::Display::fmt(e, f),
135        }
136    }
137}
138
139impl std::error::Error for ConnectError {
140    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
141        match self {
142            ConnectError::Service(e) => Some(e),
143            ConnectError::HostIdentity(e) => Some(e),
144        }
145    }
146}
147
148impl From<ServiceError> for ConnectError {
149    fn from(e: ServiceError) -> Self {
150        ConnectError::Service(e)
151    }
152}
153
154impl From<HostIdentityError> for ConnectError {
155    fn from(e: HostIdentityError) -> Self {
156        ConnectError::HostIdentity(e)
157    }
158}