use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum InvalidUriKind {
ParseError,
MissingAuthority,
MissingScheme,
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum HttpError {
#[error("Failed to build request: {0}")]
RequestBuild(#[from] http::Error),
#[error("Invalid header name: {0}")]
InvalidHeaderName(#[from] http::header::InvalidHeaderName),
#[error("Invalid header value: {0}")]
InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
#[error("Request attempt timed out after {0:?}")]
Timeout(std::time::Duration),
#[error("Operation deadline exceeded after {0:?}")]
DeadlineExceeded(std::time::Duration),
#[error("Transport error: {0}")]
Transport(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("TLS error: {0}")]
Tls(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("Response body too large: limit {limit} bytes, got {actual} bytes")]
BodyTooLarge { limit: usize, actual: usize },
#[error("HTTP {status}: {body_preview}")]
HttpStatus {
status: http::StatusCode,
body_preview: String,
content_type: Option<String>,
retry_after: Option<Duration>,
},
#[error("JSON parsing failed: {0}")]
Json(#[from] serde_json::Error),
#[error("Form encoding failed: {0}")]
FormEncode(#[from] serde_urlencoded::ser::Error),
#[error("Service overloaded: concurrency limit reached")]
Overloaded,
#[error("Service unavailable: internal failure")]
ServiceClosed,
#[error("Invalid URL '{url}': {reason}")]
InvalidUri {
url: String,
kind: InvalidUriKind,
reason: String,
},
#[error("URL scheme '{scheme}' not allowed: {reason}")]
InvalidScheme {
scheme: String,
reason: String,
},
}
impl From<hyper::Error> for HttpError {
fn from(err: hyper::Error) -> Self {
HttpError::Transport(Box::new(err))
}
}
impl From<hyper_util::client::legacy::Error> for HttpError {
fn from(err: hyper_util::client::legacy::Error) -> Self {
HttpError::Transport(Box::new(err))
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct TestError(&'static str);
impl fmt::Display for TestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Error for TestError {}
#[test]
fn test_transport_error_preserves_source() {
let inner = TestError("connection refused");
let err = HttpError::Transport(Box::new(inner));
let source = err.source();
assert!(source.is_some(), "Transport error should have a source");
let source = source.unwrap();
let downcast = source.downcast_ref::<TestError>();
assert!(
downcast.is_some(),
"Should be able to downcast to TestError"
);
assert_eq!(downcast.unwrap().0, "connection refused");
}
#[test]
fn test_tls_error_preserves_source() {
let inner = TestError("certificate expired");
let err = HttpError::Tls(Box::new(inner));
let source = err.source();
assert!(source.is_some(), "TLS error should have a source");
let source = source.unwrap();
let downcast = source.downcast_ref::<TestError>();
assert!(downcast.is_some());
assert_eq!(downcast.unwrap().0, "certificate expired");
}
#[test]
fn test_error_chain_traversal() {
let inner = TestError("root cause");
let err = HttpError::Transport(Box::new(inner));
let mut count = 0;
let mut current: Option<&(dyn Error + 'static)> = Some(&err);
while let Some(e) = current {
count += 1;
current = e.source();
}
assert_eq!(
count, 2,
"Should have 2 errors in chain: HttpError and TestError"
);
}
}