1use std::fmt;
13
14use crate::guid::Guid;
15
16pub mod codes {
19 pub const OK: i32 = 0;
20 pub const GENERIC: i32 = 1;
21 pub const CANCELED: i32 = 2;
22 pub const TIMEOUT: i32 = 3;
23 pub const TRANSPORT_ERROR: i32 = 100;
24 pub const UNAVAILABLE: i32 = 105;
25 pub const REQUEST_QUEUE_SIZE_LIMIT_EXCEEDED: i32 = 108;
26 pub const RPC_AUTHENTICATION_ERROR: i32 = 109;
27 pub const RESOLVE_ERROR: i32 = 500;
28 pub const AUTHENTICATION_ERROR: i32 = 900;
29 pub const NO_SUCH_TRANSACTION: i32 = 11000;
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct YtError {
35 pub code: i32,
36 pub message: String,
37 pub attributes: Vec<(String, Vec<u8>)>,
40 pub inner_errors: Vec<YtError>,
41}
42
43impl YtError {
44 pub fn from_proto(proto: &crate::proto::misc::TError) -> Self {
46 Self {
47 code: proto.code,
48 message: proto.message.clone().unwrap_or_default(),
49 attributes: proto
50 .attributes
51 .as_ref()
52 .map(|dictionary| {
53 dictionary
54 .attributes
55 .iter()
56 .map(|attribute| (attribute.key.clone(), attribute.value.clone()))
57 .collect()
58 })
59 .unwrap_or_default(),
60 inner_errors: proto.inner_errors.iter().map(Self::from_proto).collect(),
61 }
62 }
63
64 pub fn find(&self, code: i32) -> Option<&YtError> {
70 if self.code == code {
71 return Some(self);
72 }
73 self.inner_errors.iter().find_map(|inner| inner.find(code))
74 }
75
76 pub fn has_code(&self, code: i32) -> bool {
78 self.find(code).is_some()
79 }
80
81 pub fn innermost(&self) -> &YtError {
84 let mut current = self;
85 while let Some(first) = current.inner_errors.first() {
86 current = first;
87 }
88 current
89 }
90
91 pub fn attribute(&self, key: &str) -> Option<&[u8]> {
93 self.attributes
94 .iter()
95 .find(|(name, _)| name == key)
96 .map(|(_, value)| value.as_slice())
97 }
98}
99
100impl fmt::Display for YtError {
101 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(formatter, "{} (code {})", self.message, self.code)?;
103 for inner in &self.inner_errors {
107 let rendered = inner.to_string();
108 for line in rendered.lines() {
109 write!(formatter, "\n {line}")?;
110 }
111 }
112 Ok(())
113 }
114}
115
116impl std::error::Error for YtError {}
117
118#[derive(Debug, thiserror::Error)]
120pub enum Error {
121 #[error("{service}.{method} failed: {error}")]
127 Response {
128 service: String,
129 method: String,
130 #[source]
131 error: Box<YtError>,
132 },
133
134 #[error("bus protocol error: {0}")]
136 Packet(#[from] crate::bus::packet::PacketError),
137
138 #[error("connection to {address} failed: {source}")]
140 Connect {
141 address: String,
142 #[source]
143 source: std::io::Error,
144 },
145
146 #[error("connection lost: {0}")]
148 Io(#[from] std::io::Error),
149
150 #[error("protocol violation: {0}")]
153 Protocol(String),
154
155 #[error("could not decode {message}: {source}")]
157 Decode {
158 message: &'static str,
159 #[source]
160 source: prost::DecodeError,
161 },
162
163 #[error("{service}.{method} timed out after {timeout:?}")]
165 Timeout {
166 service: String,
167 method: String,
168 timeout: std::time::Duration,
169 },
170
171 #[error("connection closed with request {request_id} in flight")]
173 ConnectionClosed { request_id: Guid },
174
175 #[error("row wire format: {0}")]
177 Wire(#[from] crate::wire::WireError),
178}
179
180impl Error {
181 pub fn yt_error(&self) -> Option<&YtError> {
183 match self {
184 Self::Response { error, .. } => Some(error),
185 _ => None,
186 }
187 }
188
189 pub(crate) fn response(service: &str, method: &str, error: YtError) -> Self {
191 Self::Response {
192 service: service.to_owned(),
193 method: method.to_owned(),
194 error: Box::new(error),
195 }
196 }
197
198 pub fn has_code(&self, code: i32) -> bool {
201 self.yt_error().is_some_and(|error| error.has_code(code))
202 }
203}
204
205pub type Result<T> = std::result::Result<T, Error>;
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::proto;
212
213 fn proto_error(
214 code: i32,
215 message: &str,
216 inner: Vec<proto::misc::TError>,
217 ) -> proto::misc::TError {
218 proto::misc::TError {
219 code,
220 message: Some(message.to_owned()),
221 attributes: None,
222 inner_errors: inner,
223 }
224 }
225
226 #[test]
227 fn nesting_survives_the_conversion() {
228 let wire = proto_error(
229 1,
230 "lookup failed",
231 vec![proto_error(
232 1,
233 "tablet request failed",
234 vec![proto_error(codes::RESOLVE_ERROR, "no such table", vec![])],
235 )],
236 );
237
238 let error = YtError::from_proto(&wire);
239 assert_eq!(error.message, "lookup failed");
240 assert_eq!(error.inner_errors.len(), 1);
241 assert_eq!(error.innermost().message, "no such table");
242 assert_eq!(error.innermost().code, codes::RESOLVE_ERROR);
243 }
244
245 #[test]
246 fn find_reaches_an_inner_code() {
247 let wire = proto_error(
248 1,
249 "outer",
250 vec![proto_error(
251 codes::NO_SUCH_TRANSACTION,
252 "no such transaction",
253 vec![],
254 )],
255 );
256 let error = YtError::from_proto(&wire);
257
258 assert!(error.has_code(codes::NO_SUCH_TRANSACTION));
259 assert_eq!(
260 error.find(codes::NO_SUCH_TRANSACTION).unwrap().message,
261 "no such transaction"
262 );
263 assert!(!error.has_code(codes::TIMEOUT));
264 assert!(error.find(codes::TIMEOUT).is_none());
265 }
266
267 #[test]
268 fn display_indents_the_tree() {
269 let wire = proto_error(1, "outer", vec![proto_error(500, "inner", vec![])]);
270 let rendered = YtError::from_proto(&wire).to_string();
271 assert_eq!(rendered, "outer (code 1)\n inner (code 500)");
272 }
273
274 #[test]
275 fn attributes_are_kept_as_yson_bytes() {
276 let wire = proto::misc::TError {
277 code: 500,
278 message: Some("no such node".to_owned()),
279 attributes: Some(proto::ytree::TAttributeDictionary {
280 attributes: vec![proto::ytree::TAttribute {
281 key: "path".to_owned(),
282 value: b"\x01\x0c//tmp/nope".to_vec(),
283 }],
284 }),
285 inner_errors: vec![],
286 };
287 let error = YtError::from_proto(&wire);
288 assert_eq!(error.attribute("path"), Some(&b"\x01\x0c//tmp/nope"[..]));
289 assert_eq!(error.attribute("missing"), None);
290 }
291
292 #[test]
293 fn a_missing_message_reads_as_empty() {
294 let wire = proto::misc::TError {
295 code: codes::GENERIC,
296 message: None,
297 attributes: None,
298 inner_errors: vec![],
299 };
300 let error = YtError::from_proto(&wire);
301 assert_eq!(error.code, codes::GENERIC);
302 assert_eq!(error.message, "");
303 }
304
305 #[test]
306 fn has_code_reaches_through_the_crate_error() {
307 let error = Error::response(
308 "ApiService",
309 "LookupRows",
310 YtError::from_proto(&proto_error(
311 1,
312 "outer",
313 vec![proto_error(codes::NO_SUCH_TRANSACTION, "gone", vec![])],
314 )),
315 );
316 assert!(error.has_code(codes::NO_SUCH_TRANSACTION));
317 assert!(!error.has_code(codes::TIMEOUT));
318 assert!(error.yt_error().is_some());
319 assert!(error.to_string().contains("ApiService.LookupRows failed"));
320 }
321}