#![allow(
dead_code,
reason = "labels are exhaustively defined for the surface area even if some are only used under specific features"
)]
use http_extensions::HttpError;
use ohno::{ErrorLabel, Labeled};
pub(crate) const LABEL_CONNECT: ErrorLabel = ErrorLabel::from_static("connect");
pub(crate) const LABEL_REQUEST_HYPER: ErrorLabel = ErrorLabel::from_static("request_hyper");
pub(crate) const LABEL_HTTP_VERSION_UNSUPPORTED: ErrorLabel = ErrorLabel::from_static("http_version_unsupported");
pub(crate) const LABEL_TLS: ErrorLabel = ErrorLabel::from_static("tls");
pub(crate) fn collect_error_labels(error: &(dyn std::error::Error + 'static)) -> ErrorLabel {
ErrorLabel::from_error_chain(error, resolve_error_label)
}
fn resolve_error_label(error: &(dyn std::error::Error + 'static)) -> Option<ErrorLabel> {
if let Some(err) = error.downcast_ref::<HttpError>() {
return Some(err.label().clone());
}
if let Some(err) = error.downcast_ref::<std::io::Error>() {
return Some(resolve_io_error_label(err));
}
None
}
fn resolve_io_error_label(error: &std::io::Error) -> ErrorLabel {
let Some(inner) = error.get_ref() else {
return error.kind().into();
};
if let Some(inner_io) = inner.downcast_ref::<std::io::Error>() {
return resolve_io_error_label(inner_io);
}
#[cfg(any(feature = "rustls", test))]
if inner.downcast_ref::<rustls::Error>().is_some() {
return LABEL_TLS;
}
#[cfg(any(feature = "native-tls", test))]
if inner.downcast_ref::<native_tls::Error>().is_some() {
return LABEL_TLS;
}
error.kind().into()
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn collect_returns_label_for_http_error() {
let err = HttpError::other("boom", seatbelt::RecoveryInfo::never(), LABEL_CONNECT);
let label = collect_error_labels(&err);
assert!(label.as_str().contains("connect"));
}
#[test]
fn collect_handles_chained_io_error() {
let io = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
let label = collect_error_labels(&io);
assert!(!label.as_str().is_empty());
}
#[test]
fn nested_io_error_unwraps_inner_kind() {
let inner = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
let outer = std::io::Error::other(inner);
let label = resolve_io_error_label(&outer);
let kind_label: ohno::ErrorLabel = std::io::ErrorKind::ConnectionRefused.into();
assert_eq!(label.as_str(), kind_label.as_str());
}
#[test]
fn io_error_without_inner_uses_kind() {
let io = std::io::Error::from(std::io::ErrorKind::ConnectionAborted);
let label = resolve_io_error_label(&io);
let kind_label: ohno::ErrorLabel = std::io::ErrorKind::ConnectionAborted.into();
assert_eq!(label.as_str(), kind_label.as_str());
}
#[test]
#[cfg_attr(miri, ignore)]
fn native_tls_error_in_io_chain_resolves_to_tls_label() {
let Err(native_err) = native_tls::Identity::from_pkcs12(b"not-a-real-pkcs12", "wrong") else {
panic!("invalid pkcs12 must fail");
};
let io = std::io::Error::other(native_err);
let label = resolve_io_error_label(&io);
assert_eq!(label.as_str(), LABEL_TLS.as_str());
}
#[test]
fn rustls_error_in_io_chain_resolves_to_tls_label() {
let rustls_err = rustls::Error::General("synthetic".to_string());
let io = std::io::Error::other(rustls_err);
let label = resolve_io_error_label(&io);
assert_eq!(label.as_str(), LABEL_TLS.as_str());
}
#[test]
fn resolve_returns_none_for_unrecognized_error() {
#[derive(Debug)]
struct UnknownErr;
impl std::fmt::Display for UnknownErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("unknown")
}
}
impl std::error::Error for UnknownErr {}
assert!(resolve_error_label(&UnknownErr).is_none());
}
#[test]
fn resolve_returns_io_label_for_io_error() {
let io = std::io::Error::from(std::io::ErrorKind::TimedOut);
let label = resolve_error_label(&io).unwrap();
let expected: ohno::ErrorLabel = std::io::ErrorKind::TimedOut.into();
assert_eq!(label.as_str(), expected.as_str());
}
}