1use crate::host_identity::HostIdentityError;
2use serde_json::{json, Value};
3use std::fmt;
4
5pub const ERROR_HOST_HANDOFF: &str = "host_handoff";
8pub const ERROR_HOST_IDENTITY: &str = "host_identity";
12pub const ERROR_SUITE_UNRESOLVED: &str = "suite_unresolved";
15pub const ERROR_NOT_ALLOWED: &str = "request_not_allowed";
17pub const ERROR_RESPONSE_TOO_LARGE: &str = "response_too_large";
20pub const ERROR_REQUEST_TOO_LARGE: &str = "request_too_large";
24pub const ERROR_BAD_CHUNK: &str = "bad_chunk";
27pub const ERROR_CONNECTION_CLOSED: &str = "connection_closed";
29pub const ERROR_TIMEOUT: &str = "request_timeout";
33pub const ERROR_UNAVAILABLE: &str = "service_unavailable";
35
36pub const ERROR_UNAUTHENTICATED: &str = "unauthenticated";
38pub const ERROR_FORBIDDEN: &str = "forbidden";
40pub const ERROR_REVISION_CONFLICT: &str = "revision_conflict";
42pub const ERROR_RATE_LIMITED: &str = "rate_limited";
46
47#[derive(Debug, Clone, PartialEq)]
51pub struct ServiceError {
52 pub code: String,
53 pub message: String,
54 pub retryable: bool,
55 pub details: Option<Value>,
60}
61
62impl Eq for ServiceError {}
63
64impl ServiceError {
65 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
66 Self {
67 code: code.into(),
68 message: message.into(),
69 retryable: false,
70 details: None,
71 }
72 }
73 pub fn retryable(code: impl Into<String>, message: impl Into<String>) -> Self {
74 Self {
75 code: code.into(),
76 message: message.into(),
77 retryable: true,
78 details: None,
79 }
80 }
81 pub fn with_details(mut self, details: Value) -> Self {
83 self.details = if details.is_null() {
84 None
85 } else {
86 Some(details)
87 };
88 self
89 }
90 pub fn details(&self) -> Option<&Value> {
92 self.details.as_ref()
93 }
94 pub fn handoff() -> Self {
95 Self::retryable(
96 ERROR_HOST_HANDOFF,
97 "host is handing the service to a higher-ranked host; retry",
98 )
99 }
100 pub fn to_value(&self) -> Value {
101 let mut v =
102 json!({"code": self.code, "message": self.message, "retryable": self.retryable});
103 if let (Some(d), Some(o)) = (&self.details, v.as_object_mut()) {
104 o.insert("details".to_string(), d.clone());
105 }
106 v
107 }
108 pub fn from_value(v: &Value) -> Self {
109 Self {
110 code: v["code"].as_str().unwrap_or("error").into(),
111 message: v["message"].as_str().unwrap_or("").into(),
112 retryable: v["retryable"].as_bool().unwrap_or(false),
113 details: v.get("details").filter(|d| !d.is_null()).cloned(),
114 }
115 }
116}
117
118impl fmt::Display for ServiceError {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 write!(f, "{}: {}", self.code, self.message)
121 }
122}
123impl std::error::Error for ServiceError {}
124impl From<std::io::Error> for ServiceError {
125 fn from(e: std::io::Error) -> Self {
126 Self::new("io", e.to_string())
127 }
128}
129impl From<HostIdentityError> for ServiceError {
130 fn from(e: HostIdentityError) -> Self {
132 Self::new(ERROR_HOST_IDENTITY, e.to_string())
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
143#[non_exhaustive]
144pub enum ConnectError {
145 Service(ServiceError),
146 HostIdentity(HostIdentityError),
147}
148
149impl ConnectError {
150 pub fn into_service_error(self) -> ServiceError {
153 match self {
154 ConnectError::Service(e) => e,
155 ConnectError::HostIdentity(e) => e.into(),
156 }
157 }
158}
159
160impl fmt::Display for ConnectError {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 match self {
163 ConnectError::Service(e) => fmt::Display::fmt(e, f),
164 ConnectError::HostIdentity(e) => fmt::Display::fmt(e, f),
165 }
166 }
167}
168
169impl std::error::Error for ConnectError {
170 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
171 match self {
172 ConnectError::Service(e) => Some(e),
173 ConnectError::HostIdentity(e) => Some(e),
174 }
175 }
176}
177
178impl From<ServiceError> for ConnectError {
179 fn from(e: ServiceError) -> Self {
180 ConnectError::Service(e)
181 }
182}
183
184impl From<HostIdentityError> for ConnectError {
185 fn from(e: HostIdentityError) -> Self {
186 ConnectError::HostIdentity(e)
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn details_roundtrip_and_absent_or_null_read_as_none() {
196 let plain = ServiceError::new("nope", "x");
197 assert!(plain.details().is_none());
198 assert!(plain.to_value().get("details").is_none());
199 assert_eq!(ServiceError::from_value(&plain.to_value()), plain);
200 let nulled = json!({"code": "nope", "message": "x", "retryable": false, "details": null});
201 assert_eq!(ServiceError::from_value(&nulled), plain);
202 let detailed = ServiceError::retryable("needs_auth", "sign in")
203 .with_details(json!({"platform": "x", "nested": {"a": [1, 2]}}));
204 assert_eq!(detailed.to_value()["details"]["nested"]["a"], json!([1, 2]));
205 assert_eq!(ServiceError::from_value(&detailed.to_value()), detailed);
206 assert!(detailed.with_details(Value::Null).details().is_none());
207 }
208}