use std::sync::Arc;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
pub enum OxiHttpError {
#[error("invalid URI: {0}")]
InvalidUri(Arc<http::uri::InvalidUri>),
#[error("HTTP error: {0}")]
Http(Arc<http::Error>),
#[error("hyper error: {0}")]
Hyper(String),
#[error("I/O error: {0}")]
Io(Arc<std::io::Error>),
#[error("body error: {0}")]
Body(String),
#[error("timeout: {0}")]
Timeout(String),
#[error("redirect error: {0}")]
Redirect(String),
#[error("TLS error: {0}")]
Tls(String),
#[error("DNS error: {0}")]
Dns(String),
#[error("connection pool error: {0}")]
ConnectionPool(String),
#[error("JSON error: {0}")]
Json(String),
#[error("form encoding error: {0}")]
FormEncoding(String),
#[error("invalid header: {0}")]
InvalidHeader(String),
#[error("server error: {0}")]
Server(String),
#[error("route not found: {method} {path}")]
RouteNotFound {
method: String,
path: String,
},
#[error("method not allowed: {method} {path}")]
MethodNotAllowed {
method: String,
path: String,
},
#[error("HTTP/3 error: {0}")]
H3(String),
}
impl From<http::uri::InvalidUri> for OxiHttpError {
fn from(e: http::uri::InvalidUri) -> Self {
OxiHttpError::InvalidUri(Arc::new(e))
}
}
impl From<std::io::Error> for OxiHttpError {
fn from(e: std::io::Error) -> Self {
OxiHttpError::Io(Arc::new(e))
}
}
impl From<http::Error> for OxiHttpError {
fn from(e: http::Error) -> Self {
OxiHttpError::Http(Arc::new(e))
}
}
#[cfg(feature = "tls")]
impl From<oxitls_core::TlsError> for OxiHttpError {
fn from(e: oxitls_core::TlsError) -> Self {
OxiHttpError::Tls(e.to_string())
}
}
impl OxiHttpError {
pub fn status_code(&self) -> Option<http::StatusCode> {
match self {
Self::RouteNotFound { .. } => Some(http::StatusCode::NOT_FOUND),
Self::MethodNotAllowed { .. } => Some(http::StatusCode::METHOD_NOT_ALLOWED),
Self::Timeout(_) => Some(http::StatusCode::REQUEST_TIMEOUT),
_ => None,
}
}
pub fn is_timeout(&self) -> bool {
matches!(self, Self::Timeout(_))
}
pub fn is_connect(&self) -> bool {
matches!(self, Self::Dns(_) | Self::ConnectionPool(_) | Self::Tls(_))
}
pub fn is_body(&self) -> bool {
matches!(self, Self::Body(_))
}
pub fn is_redirect(&self) -> bool {
matches!(self, Self::Redirect(_))
}
}
#[cfg(test)]
mod clone_tests {
use super::*;
#[test]
fn test_oxi_http_error_is_clone() {
let io_err = OxiHttpError::from(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
let cloned = io_err.clone();
assert_eq!(io_err.to_string(), cloned.to_string());
let str_err = OxiHttpError::Body("test".to_string());
let _ = str_err.clone();
}
}
#[cfg(test)]
mod error_tests {
use super::*;
#[test]
fn test_display_invalid_uri() {
let raw_err: http::uri::InvalidUri = "not a valid uri!!!"
.parse::<http::Uri>()
.expect_err("should fail to parse");
let err = OxiHttpError::from(raw_err);
let msg = err.to_string();
assert!(
msg.contains("invalid URI"),
"expected 'invalid URI' in '{msg}'"
);
}
#[test]
fn test_display_http_error() {
let raw_err = http::Request::builder()
.header("\n", "x")
.body(())
.expect_err("should fail with invalid header name");
let err = OxiHttpError::from(raw_err);
let msg = err.to_string();
assert!(
msg.contains("HTTP error"),
"expected 'HTTP error' in '{msg}'"
);
}
#[test]
fn test_display_hyper_error() {
let err = OxiHttpError::Hyper("connection reset".to_string());
let msg = err.to_string();
assert!(
msg.contains("hyper error"),
"expected 'hyper error' in '{msg}'"
);
}
#[test]
fn test_display_io_error() {
let raw_err = std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"connection refused test",
);
let err = OxiHttpError::from(raw_err);
let msg = err.to_string();
assert!(msg.contains("I/O error"), "expected 'I/O error' in '{msg}'");
}
#[test]
fn test_display_body_error() {
let err = OxiHttpError::Body("chunk too large".to_string());
let msg = err.to_string();
assert!(
msg.contains("body error"),
"expected 'body error' in '{msg}'"
);
}
#[test]
fn test_display_timeout() {
let err = OxiHttpError::Timeout("request timed out".to_string());
let msg = err.to_string();
assert!(msg.contains("timeout"), "expected 'timeout' in '{msg}'");
}
#[test]
fn test_display_redirect() {
let err = OxiHttpError::Redirect("too many redirects".to_string());
let msg = err.to_string();
assert!(
msg.contains("redirect error"),
"expected 'redirect error' in '{msg}'"
);
}
#[test]
fn test_display_tls() {
let err = OxiHttpError::Tls("certificate invalid".to_string());
let msg = err.to_string();
assert!(msg.contains("TLS error"), "expected 'TLS error' in '{msg}'");
}
#[test]
fn test_display_dns() {
let err = OxiHttpError::Dns("no such host".to_string());
let msg = err.to_string();
assert!(msg.contains("DNS error"), "expected 'DNS error' in '{msg}'");
}
#[test]
fn test_display_connection_pool() {
let err = OxiHttpError::ConnectionPool("pool exhausted".to_string());
let msg = err.to_string();
assert!(
msg.contains("connection pool error"),
"expected 'connection pool error' in '{msg}'"
);
}
#[test]
fn test_display_json() {
let err = OxiHttpError::Json("unexpected token".to_string());
let msg = err.to_string();
assert!(
msg.contains("JSON error"),
"expected 'JSON error' in '{msg}'"
);
}
#[test]
fn test_display_route_not_found() {
let err = OxiHttpError::RouteNotFound {
method: "GET".to_string(),
path: "/foo".to_string(),
};
let msg = err.to_string();
assert!(
msg.contains("route not found"),
"expected 'route not found' in '{msg}'"
);
assert!(msg.contains("GET"), "expected 'GET' in '{msg}'");
assert!(msg.contains("/foo"), "expected '/foo' in '{msg}'");
}
#[test]
fn test_display_method_not_allowed() {
let err = OxiHttpError::MethodNotAllowed {
method: "DELETE".to_string(),
path: "/bar".to_string(),
};
let msg = err.to_string();
assert!(
msg.contains("method not allowed"),
"expected 'method not allowed' in '{msg}'"
);
assert!(msg.contains("DELETE"), "expected 'DELETE' in '{msg}'");
assert!(msg.contains("/bar"), "expected '/bar' in '{msg}'");
}
#[test]
fn test_from_invalid_uri() {
let raw: http::uri::InvalidUri = "not a valid uri!!!"
.parse::<http::Uri>()
.expect_err("should fail");
let result = OxiHttpError::from(raw);
assert!(
matches!(result, OxiHttpError::InvalidUri(_)),
"expected InvalidUri variant"
);
}
#[test]
fn test_from_http_error() {
let raw = http::Request::builder()
.header("\n", "x")
.body(())
.expect_err("should fail with invalid header name");
let result = OxiHttpError::from(raw);
assert!(
matches!(result, OxiHttpError::Http(_)),
"expected Http variant"
);
}
#[test]
fn test_from_io_error() {
let raw = std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"test io error message",
);
let result = OxiHttpError::from(raw);
assert!(matches!(result, OxiHttpError::Io(_)), "expected Io variant");
assert!(
result.to_string().contains("test io error message"),
"Display should include the original io message"
);
}
#[test]
fn test_status_code_route_not_found() {
let err = OxiHttpError::RouteNotFound {
method: "GET".to_string(),
path: "/missing".to_string(),
};
assert_eq!(err.status_code(), Some(http::StatusCode::NOT_FOUND));
}
#[test]
fn test_status_code_method_not_allowed() {
let err = OxiHttpError::MethodNotAllowed {
method: "PUT".to_string(),
path: "/resource".to_string(),
};
assert_eq!(
err.status_code(),
Some(http::StatusCode::METHOD_NOT_ALLOWED)
);
}
#[test]
fn test_status_code_timeout() {
let err = OxiHttpError::Timeout("waited too long".to_string());
assert_eq!(err.status_code(), Some(http::StatusCode::REQUEST_TIMEOUT));
}
#[test]
fn test_status_code_body_is_none() {
let err = OxiHttpError::Body("incomplete body".to_string());
assert_eq!(err.status_code(), None);
}
#[test]
fn test_is_timeout_true() {
let err = OxiHttpError::Timeout("timed out".to_string());
assert!(err.is_timeout());
}
#[test]
fn test_is_timeout_false() {
let err = OxiHttpError::Body("body error".to_string());
assert!(!err.is_timeout());
}
#[test]
fn test_is_connect_dns() {
let err = OxiHttpError::Dns("nxdomain".to_string());
assert!(err.is_connect());
}
#[test]
fn test_is_connect_pool() {
let err = OxiHttpError::ConnectionPool("exhausted".to_string());
assert!(err.is_connect());
}
#[test]
fn test_is_connect_tls() {
let err = OxiHttpError::Tls("bad cert".to_string());
assert!(err.is_connect());
}
#[test]
fn test_is_connect_false() {
let err = OxiHttpError::Timeout("timed out".to_string());
assert!(!err.is_connect());
}
#[test]
fn test_is_body_true() {
let err = OxiHttpError::Body("truncated".to_string());
assert!(err.is_body());
}
#[test]
fn test_is_body_false() {
let err = OxiHttpError::Json("bad json".to_string());
assert!(!err.is_body());
}
#[test]
fn test_is_redirect_true() {
let err = OxiHttpError::Redirect("loop detected".to_string());
assert!(err.is_redirect());
}
#[test]
fn test_is_redirect_false() {
let err = OxiHttpError::Timeout("timed out".to_string());
assert!(!err.is_redirect());
}
}