1use core::{convert::Infallible, fmt};
2
3use rama_core::{
4 error::{BoxError, ErrorExt as _, extra::OpaqueError},
5 telemetry::tracing,
6};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum ConnectionErrorDomain {
17 Transport,
23 Application,
33 Local,
41 Unknown,
44}
45
46impl fmt::Display for ConnectionErrorDomain {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 f.write_str(match self {
49 Self::Transport => "transport",
50 Self::Application => "application",
51 Self::Local => "local",
52 Self::Unknown => "unknown",
53 })
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59#[non_exhaustive]
60pub enum ConnectionErrorKind {
61 Unavailable,
63 Timeout,
65 Rejected,
67 Authentication,
69 Protocol,
71 InvalidInput,
73 Internal,
75 Other,
77}
78
79impl fmt::Display for ConnectionErrorKind {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 f.write_str(match self {
82 Self::Unavailable => "unavailable",
83 Self::Timeout => "timeout",
84 Self::Rejected => "rejected",
85 Self::Authentication => "authentication",
86 Self::Protocol => "protocol",
87 Self::InvalidInput => "invalid-input",
88 Self::Internal => "internal",
89 Self::Other => "other",
90 })
91 }
92}
93
94#[must_use = "a connection error should be returned or handled"]
99pub struct ConnectionError {
100 source: BoxError,
101 domain: ConnectionErrorDomain,
102 kind: ConnectionErrorKind,
103}
104
105impl ConnectionError {
106 pub fn new(
108 source: impl Into<BoxError>,
109 domain: ConnectionErrorDomain,
110 kind: ConnectionErrorKind,
111 ) -> Self {
112 Self {
113 source: source.into(),
114 domain,
115 kind,
116 }
117 }
118
119 pub fn transport(source: impl Into<BoxError>, kind: ConnectionErrorKind) -> Self {
121 Self::new(source, ConnectionErrorDomain::Transport, kind)
122 }
123
124 pub fn application(source: impl Into<BoxError>, kind: ConnectionErrorKind) -> Self {
129 Self::new(source, ConnectionErrorDomain::Application, kind)
130 }
131
132 pub fn local(source: impl Into<BoxError>, kind: ConnectionErrorKind) -> Self {
134 Self::new(source, ConnectionErrorDomain::Local, kind)
135 }
136
137 pub fn unknown(source: impl Into<BoxError>) -> Self {
139 Self::new(
140 source,
141 ConnectionErrorDomain::Unknown,
142 ConnectionErrorKind::Other,
143 )
144 }
145
146 #[inline]
148 pub fn domain(&self) -> ConnectionErrorDomain {
149 self.domain
150 }
151
152 #[inline]
154 pub fn kind(&self) -> ConnectionErrorKind {
155 self.kind
156 }
157
158 #[inline]
160 pub fn get_ref(&self) -> &(dyn core::error::Error + Send + Sync + 'static) {
161 self.source.as_ref()
162 }
163
164 #[inline]
166 pub fn into_source(self) -> BoxError {
167 self.source
168 }
169
170 #[inline]
172 pub fn into_box_error(self) -> BoxError {
173 Box::new(self)
174 }
175
176 #[inline]
178 pub fn into_opaque_error(self) -> OpaqueError {
179 self.into_box_error().into_opaque_error()
180 }
181
182 pub fn context<M>(mut self, value: M) -> Self
184 where
185 M: fmt::Debug + fmt::Display + Send + Sync + 'static,
186 {
187 self.source = self.source.context(value);
188 self
189 }
190
191 pub fn context_hex<M>(mut self, value: M) -> Self
193 where
194 M: fmt::Debug + Send + Sync + 'static,
195 {
196 self.source = self.source.context_hex(value);
197 self
198 }
199
200 pub fn context_debug<M>(mut self, value: M) -> Self
202 where
203 M: fmt::Debug + Send + Sync + 'static,
204 {
205 self.source = self.source.context_debug(value);
206 self
207 }
208
209 pub fn context_field<M>(mut self, key: &'static str, value: M) -> Self
211 where
212 M: fmt::Debug + fmt::Display + Send + Sync + 'static,
213 {
214 self.source = self.source.context_field(key, value);
215 self
216 }
217
218 pub fn context_str_field<M>(mut self, key: &'static str, value: M) -> Self
220 where
221 M: Into<String>,
222 {
223 self.source = self.source.context_str_field(key, value);
224 self
225 }
226
227 pub fn context_hex_field<M>(mut self, key: &'static str, value: M) -> Self
229 where
230 M: fmt::Debug + Send + Sync + 'static,
231 {
232 self.source = self.source.context_hex_field(key, value);
233 self
234 }
235
236 pub fn context_debug_field<M>(mut self, key: &'static str, value: M) -> Self
238 where
239 M: fmt::Debug + Send + Sync + 'static,
240 {
241 self.source = self.source.context_debug_field(key, value);
242 self
243 }
244
245 pub fn with_context<C, F>(mut self, create: F) -> Self
247 where
248 C: fmt::Debug + fmt::Display + Send + Sync + 'static,
249 F: FnOnce() -> C,
250 {
251 self.source = self.source.with_context(create);
252 self
253 }
254
255 pub fn with_context_hex<C, F>(mut self, create: F) -> Self
257 where
258 C: fmt::Debug + Send + Sync + 'static,
259 F: FnOnce() -> C,
260 {
261 self.source = self.source.with_context_hex(create);
262 self
263 }
264
265 pub fn with_context_debug<C, F>(mut self, create: F) -> Self
267 where
268 C: fmt::Debug + Send + Sync + 'static,
269 F: FnOnce() -> C,
270 {
271 self.source = self.source.with_context_debug(create);
272 self
273 }
274
275 pub fn with_context_field<C, F>(mut self, key: &'static str, create: F) -> Self
277 where
278 C: fmt::Debug + fmt::Display + Send + Sync + 'static,
279 F: FnOnce() -> C,
280 {
281 self.source = self.source.with_context_field(key, create);
282 self
283 }
284
285 pub fn with_context_str_field<C, F>(mut self, key: &'static str, create: F) -> Self
287 where
288 C: Into<String>,
289 F: FnOnce() -> C,
290 {
291 self.source = self.source.with_context_str_field(key, create);
292 self
293 }
294
295 pub fn with_context_hex_field<C, F>(mut self, key: &'static str, create: F) -> Self
297 where
298 C: fmt::Debug + Send + Sync + 'static,
299 F: FnOnce() -> C,
300 {
301 self.source = self.source.with_context_hex_field(key, create);
302 self
303 }
304
305 pub fn with_context_debug_field<C, F>(mut self, key: &'static str, create: F) -> Self
307 where
308 C: fmt::Debug + Send + Sync + 'static,
309 F: FnOnce() -> C,
310 {
311 self.source = self.source.with_context_debug_field(key, create);
312 self
313 }
314
315 pub fn backtrace(mut self) -> Self {
317 self.source = self.source.backtrace();
318 self
319 }
320
321 fn classification_in_source_chain(
322 source: &(dyn core::error::Error + 'static),
323 ) -> Option<(ConnectionErrorDomain, ConnectionErrorKind)> {
324 let mut current = Some(source);
325 let mut timeout = false;
326 for _ in 0..64 {
329 let Some(error) = current else {
330 break;
331 };
332 if let Some(error) = error.downcast_ref::<Self>() {
333 return Some((error.domain, error.kind));
334 }
335 timeout |= error.is::<rama_core::layer::timeout::Elapsed>()
336 || error.is::<tokio::time::error::Elapsed>();
337 current = error.source();
338 }
339 timeout.then_some((
340 ConnectionErrorDomain::Transport,
341 ConnectionErrorKind::Timeout,
342 ))
343 }
344}
345
346impl From<BoxError> for ConnectionError {
347 fn from(source: BoxError) -> Self {
348 let source = match source.downcast::<Self>() {
349 Ok(error) => return *error,
350 Err(source) => source,
351 };
352
353 let (domain, kind) =
354 Self::classification_in_source_chain(source.as_ref()).unwrap_or_else(|| {
355 tracing::debug!(
356 "connector error is unclassified; retry-based routing will treat it as terminal"
357 );
358 (ConnectionErrorDomain::Unknown, ConnectionErrorKind::Other)
359 });
360
361 Self {
362 source,
363 domain,
364 kind,
365 }
366 }
367}
368
369impl From<Infallible> for ConnectionError {
370 fn from(error: Infallible) -> Self {
371 match error {}
372 }
373}
374
375impl fmt::Debug for ConnectionError {
376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377 f.debug_struct("ConnectionError")
378 .field("domain", &self.domain)
379 .field("kind", &self.kind)
380 .field("source", &self.source)
381 .finish()
382 }
383}
384
385impl fmt::Display for ConnectionError {
386 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387 fmt::Display::fmt(&self.source, f)
388 }
389}
390
391impl core::error::Error for ConnectionError {
392 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
393 Some(self.source.as_ref())
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use rama_core::{Layer, Service, error::ErrorContext as _};
401
402 #[derive(Debug)]
403 struct TestError(&'static str);
404
405 impl fmt::Display for TestError {
406 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
407 f.write_str(self.0)
408 }
409 }
410
411 impl core::error::Error for TestError {}
412
413 #[derive(Debug)]
414 struct CyclicError;
415
416 impl fmt::Display for CyclicError {
417 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418 f.write_str("cyclic error")
419 }
420 }
421
422 impl core::error::Error for CyclicError {
423 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
424 Some(self)
425 }
426 }
427
428 fn unavailable_transport_error() -> ConnectionError {
429 ConnectionError::transport(
430 TestError("connect failed"),
431 ConnectionErrorKind::Unavailable,
432 )
433 }
434
435 fn assert_unavailable_transport(error: &ConnectionError) {
436 assert_eq!(error.domain(), ConnectionErrorDomain::Transport);
437 assert_eq!(error.kind(), ConnectionErrorKind::Unavailable);
438 }
439
440 #[test]
441 fn display_preserves_source_message() {
442 let error = unavailable_transport_error();
443 assert_eq!(error.to_string(), "connect failed");
444 assert!(error.get_ref().is::<TestError>());
445 }
446
447 #[test]
448 fn direct_box_roundtrip_preserves_classification() {
449 let error = ConnectionError::from(unavailable_transport_error().into_box_error());
450 assert_unavailable_transport(&error);
451 assert!(error.get_ref().is::<TestError>());
452 }
453
454 #[test]
455 fn contextual_box_roundtrip_preserves_classification() {
456 let boxed = unavailable_transport_error()
457 .into_box_error()
458 .context("dial selected endpoint")
459 .context_field("attempt", 2);
460
461 let error = ConnectionError::from(boxed);
462 assert_unavailable_transport(&error);
463 let message = error.to_string();
464 assert!(message.contains("dial selected endpoint"), "{message}");
465 assert!(message.contains("attempt=\"2\""), "{message}");
466 }
467
468 #[tokio::test(start_paused = true)]
469 async fn timeout_layer_error_is_classified_as_transport_timeout() {
470 let source =
471 rama_core::layer::timeout::TimeoutLayer::new(core::time::Duration::from_secs(1));
472 let error = source
473 .into_layer(rama_core::service::service_fn(async |(): ()| {
474 core::future::pending::<Result<(), Infallible>>().await
475 }))
476 .serve(())
477 .await
478 .unwrap_err();
479
480 let error = ConnectionError::from(error.context("connector attempt"));
481 assert_eq!(error.domain(), ConnectionErrorDomain::Transport);
482 assert_eq!(error.kind(), ConnectionErrorKind::Timeout);
483 }
484
485 #[test]
486 fn result_context_roundtrip_preserves_classification() {
487 fn contextualized() -> Result<(), ConnectionError> {
488 let result: Result<(), ConnectionError> = Err(unavailable_transport_error());
489 result.context("establish connection")?;
490 Ok(())
491 }
492
493 let error = contextualized().unwrap_err();
494 assert_unavailable_transport(&error);
495 assert!(error.to_string().contains("establish connection"));
496 }
497
498 #[test]
499 fn inherent_context_api_preserves_classification() {
500 let error = unavailable_transport_error()
501 .context("context")
502 .context_hex(255)
503 .context_debug(Some("debug"))
504 .context_field("field", 1)
505 .context_str_field("str", "value")
506 .context_hex_field("hex", 255)
507 .context_debug_field("debug", Some(2))
508 .with_context(|| "lazy-context")
509 .with_context_hex(|| 16)
510 .with_context_debug(|| Some("lazy-debug"))
511 .with_context_field("lazy-field", || 3)
512 .with_context_str_field("lazy-str", || "lazy-value")
513 .with_context_hex_field("lazy-hex", || 32)
514 .with_context_debug_field("lazy-debug-field", || Some(4));
515
516 assert_unavailable_transport(&error);
517 let message = error.to_string();
518 for expected in [
519 "context",
520 "field=\"1\"",
521 "str=\"value\"",
522 "lazy-context",
523 "lazy-field=\"3\"",
524 "lazy-str=\"lazy-value\"",
525 ] {
526 assert!(
527 message.contains(expected),
528 "missing {expected:?} in {message}"
529 );
530 }
531 }
532
533 #[test]
534 fn unknown_box_error_gets_safe_classification() {
535 let source: BoxError = Box::new(TestError("legacy error"));
536 let error = ConnectionError::from(source);
537
538 assert_eq!(error.domain(), ConnectionErrorDomain::Unknown);
539 assert_eq!(error.kind(), ConnectionErrorKind::Other);
540 assert_eq!(error.to_string(), "legacy error");
541 }
542
543 #[test]
544 fn cyclic_source_chain_gets_safe_classification() {
545 let source: BoxError = Box::new(CyclicError);
546 let error = ConnectionError::from(source);
547
548 assert_eq!(error.domain(), ConnectionErrorDomain::Unknown);
549 assert_eq!(error.kind(), ConnectionErrorKind::Other);
550 assert_eq!(error.to_string(), "cyclic error");
551 }
552
553 #[test]
554 fn backtrace_and_opaque_roundtrip_preserve_classification() {
555 let opaque = unavailable_transport_error()
556 .backtrace()
557 .into_opaque_error();
558 let error = ConnectionError::from(opaque.into_box_error());
559
560 assert_unavailable_transport(&error);
561 assert_eq!(error.to_string(), "connect failed");
562 }
563}