use bytes::Bytes;
use http_body_util::combinators::UnsyncBoxBody;
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("HTTP error: {0}")]
Http(#[from] http::Error),
#[error("hyper error: {0}")]
Hyper(#[from] hyper::Error),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("TLS error: {0}")]
Tls(BoxError),
#[error("request timeout")]
Timeout,
#[error("invalid URL: {0}")]
InvalidUrl(String),
#[error("HTTP status error: {0}")]
Status(http::StatusCode),
#[error("redirect error: {0}")]
Redirect(String),
#[error("too many redirects (max {0})")]
TooManyRedirects(usize),
#[error("HTTPS required but URL scheme is {0}")]
HttpsOnly(String),
#[error("invalid header: {0}")]
InvalidHeader(String),
#[error("{0}")]
Other(BoxError),
}
pub type AioductBody = UnsyncBoxBody<Bytes, Error>;
impl Error {
pub fn is_connect(&self) -> bool {
matches!(self, Error::Io(_) | Error::Tls(_) | Error::Timeout)
}
pub fn is_timeout(&self) -> bool {
matches!(self, Error::Timeout)
}
pub fn is_status(&self) -> bool {
matches!(self, Error::Status(_))
}
pub fn status(&self) -> Option<http::StatusCode> {
match self {
Error::Status(code) => Some(*code),
_ => None,
}
}
pub fn is_redirect(&self) -> bool {
matches!(self, Error::Redirect(_) | Error::TooManyRedirects(_))
}
pub fn is_closed(&self) -> bool {
use std::error::Error as _;
match self {
Error::Hyper(e) => {
if e.is_canceled() || e.is_closed() || e.is_incomplete_message() {
return true;
}
if let Some(io_err) = e.source().and_then(|s| s.downcast_ref::<std::io::Error>()) {
return matches!(
io_err.kind(),
std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::BrokenPipe
| std::io::ErrorKind::ConnectionAborted
);
}
false
}
Error::Io(e) => matches!(
e.kind(),
std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::BrokenPipe
| std::io::ErrorKind::ConnectionAborted
),
_ => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_connect_for_io() {
let err = Error::Io(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"refused",
));
assert!(err.is_connect());
assert!(!err.is_status());
assert!(!err.is_timeout());
assert!(!err.is_redirect());
}
#[test]
fn is_connect_for_tls() {
let err = Error::Tls("bad cert".into());
assert!(err.is_connect());
}
#[test]
fn is_connect_for_timeout() {
let err = Error::Timeout;
assert!(err.is_connect());
assert!(err.is_timeout());
}
#[test]
fn is_status_and_status_accessor() {
let err = Error::Status(http::StatusCode::NOT_FOUND);
assert!(err.is_status());
assert_eq!(err.status(), Some(http::StatusCode::NOT_FOUND));
}
#[test]
fn status_returns_none_for_non_status() {
let err = Error::Timeout;
assert_eq!(err.status(), None);
}
#[test]
fn is_redirect_for_redirect() {
let err = Error::Redirect("missing Location".into());
assert!(err.is_redirect());
}
#[test]
fn is_redirect_for_too_many() {
let err = Error::TooManyRedirects(10);
assert!(err.is_redirect());
}
#[test]
fn non_connect_errors() {
assert!(!Error::Status(http::StatusCode::OK).is_connect());
assert!(!Error::InvalidUrl("bad".into()).is_connect());
assert!(!Error::Redirect("nope".into()).is_connect());
assert!(!Error::TooManyRedirects(5).is_connect());
assert!(!Error::HttpsOnly("http".into()).is_connect());
assert!(!Error::InvalidHeader("bad".into()).is_connect());
assert!(!Error::Other("misc".into()).is_connect());
}
#[test]
fn display_formats() {
assert_eq!(Error::Timeout.to_string(), "request timeout");
assert!(Error::TooManyRedirects(10).to_string().contains("10"));
assert!(Error::HttpsOnly("http".into()).to_string().contains("http"));
}
}