ugi 0.2.1

Runtime-agnostic Rust request client with HTTP/1.1, HTTP/2, HTTP/3, H2C, WebSocket, SSE, and gRPC support
Documentation
use std::error::Error as StdError;
use std::fmt;

pub type Result<T, E = Error> = std::result::Result<T, E>;

/// Categorises the cause of an [`Error`].
///
/// Use this to distinguish recoverable conditions (e.g. `StaleConnection`,
/// which is safe to retry) from hard failures.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ErrorKind {
    /// The supplied URL could not be parsed.
    InvalidUrl,
    /// A header name was syntactically invalid.
    InvalidHeaderName,
    /// A header value was syntactically invalid.
    InvalidHeaderValue,
    /// The request body was already consumed and cannot be read a second time.
    BodyAlreadyConsumed,
    /// A compression or serialisation decode failed.
    Decode,
    /// A network-level or protocol-level failure occurred.
    Transport,
    /// The server closed a reused connection before any response bytes were
    /// received.  Callers that implement retry-on-stale logic should match
    /// this variant rather than inspecting error message strings.
    StaleConnection,
    /// A timeout elapsed before the operation completed.
    Timeout,
}

/// An error returned by ugi operations.
///
/// Every error carries a [`kind`](Self::kind) that can be matched on and a
/// human-readable message.  An optional source error is available through the
/// standard [`std::error::Error::source`] chain.
#[derive(Debug)]
pub struct Error {
    kind: ErrorKind,
    message: String,
    source: Option<Box<dyn StdError + Send + Sync>>,
}

impl Error {
    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
        Self {
            kind,
            message: message.into(),
            source: None,
        }
    }

    pub fn with_source(
        kind: ErrorKind,
        message: impl Into<String>,
        source: impl StdError + Send + Sync + 'static,
    ) -> Self {
        Self {
            kind,
            message: message.into(),
            source: Some(Box::new(source)),
        }
    }

    pub fn kind(&self) -> &ErrorKind {
        &self.kind
    }

    /// Prepend `"METHOD URL: "` context to the error message.
    ///
    /// Used at execute-boundary call sites so that every transport error
    /// surfaced to callers carries the originating request coordinates.
    pub(crate) fn with_request_context(self, method: &str, url: &str) -> Self {
        Self {
            kind: self.kind,
            message: format!("{method} {url}: {}", self.message),
            source: self.source,
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}: {}", self.kind, self.message)
    }
}

impl StdError for Error {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        self.source
            .as_deref()
            .map(|err| err as &(dyn StdError + 'static))
    }
}

impl fmt::Display for ErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let name = match self {
            ErrorKind::InvalidUrl => "invalid_url",
            ErrorKind::InvalidHeaderName => "invalid_header_name",
            ErrorKind::InvalidHeaderValue => "invalid_header_value",
            ErrorKind::BodyAlreadyConsumed => "body_already_consumed",
            ErrorKind::Decode => "decode",
            ErrorKind::Transport => "transport",
            ErrorKind::StaleConnection => "stale_connection",
            ErrorKind::Timeout => "timeout",
        };
        f.write_str(name)
    }
}