1use thiserror::Error;
4
5#[derive(Debug, Error)]
7pub enum HttpError {
8 #[error("HTTP error ({status}): {message}")]
10 Status {
11 status: u16,
13 message: String,
15 },
16 #[error("Connection error: {0}")]
18 Connection(String),
19 #[error("Request timeout")]
21 Timeout,
22 #[error("Serialization error: {0}")]
24 Serialization(String),
25 #[error("Proxy error: {0}")]
27 Proxy(String),
28 #[error("Client build error: {0}")]
30 Build(String),
31 #[error("{0}")]
33 Other(String),
34}
35
36impl HttpError {
37 pub fn is_replay_safe(&self) -> bool {
44 matches!(self, Self::Connection(_) | Self::Timeout)
45 }
46}
47
48#[cfg(all(
49 feature = "bitreq",
50 not(feature = "reqwest"),
51 not(target_arch = "wasm32")
52))]
53impl From<bitreq::Error> for HttpError {
54 fn from(err: bitreq::Error) -> Self {
55 use std::io;
56
57 use bitreq::Error;
58
59 match err {
60 Error::SerdeJsonError(_) => HttpError::Serialization(err.to_string()),
61 Error::InvalidUtf8InBody(_) => HttpError::Serialization(err.to_string()),
62 Error::InvalidUtf8InResponse => HttpError::Serialization(err.to_string()),
63 Error::IoError(io_err) => {
64 if io_err.kind() == io::ErrorKind::TimedOut {
65 HttpError::Timeout
66 } else if io_err.kind() == io::ErrorKind::ConnectionRefused
67 || io_err.kind() == io::ErrorKind::ConnectionReset
68 || io_err.kind() == io::ErrorKind::ConnectionAborted
69 || io_err.kind() == io::ErrorKind::NotConnected
70 {
71 HttpError::Connection(io_err.to_string())
72 } else {
73 HttpError::Other(io_err.to_string())
74 }
75 }
76 Error::AddressNotFound => HttpError::Connection(err.to_string()),
77 _ => HttpError::Other(err.to_string()),
78 }
79 }
80}
81
82impl From<serde_json::Error> for HttpError {
83 fn from(err: serde_json::Error) -> Self {
84 HttpError::Serialization(err.to_string())
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn test_http_error_status_display() {
94 let error = HttpError::Status {
95 status: 404,
96 message: "Not Found".to_string(),
97 };
98 assert_eq!(format!("{}", error), "HTTP error (404): Not Found");
99 }
100
101 #[test]
102 fn test_http_error_connection_display() {
103 let error = HttpError::Connection("connection refused".to_string());
104 assert_eq!(format!("{}", error), "Connection error: connection refused");
105 }
106
107 #[test]
108 fn test_http_error_timeout_display() {
109 let error = HttpError::Timeout;
110 assert_eq!(format!("{}", error), "Request timeout");
111 }
112
113 #[test]
114 fn test_http_error_serialization_display() {
115 let error = HttpError::Serialization("invalid JSON".to_string());
116 assert_eq!(format!("{}", error), "Serialization error: invalid JSON");
117 }
118
119 #[test]
120 fn test_http_error_proxy_display() {
121 let error = HttpError::Proxy("proxy unreachable".to_string());
122 assert_eq!(format!("{}", error), "Proxy error: proxy unreachable");
123 }
124
125 #[test]
126 fn test_http_error_build_display() {
127 let error = HttpError::Build("invalid config".to_string());
128 assert_eq!(format!("{}", error), "Client build error: invalid config");
129 }
130
131 #[test]
132 fn test_http_error_other_display() {
133 let error = HttpError::Other("unknown error".to_string());
134 assert_eq!(format!("{}", error), "unknown error");
135 }
136
137 #[test]
138 fn test_from_serde_json_error() {
139 let result: Result<String, _> = serde_json::from_str("not valid json");
141 let json_error = result.expect_err("Invalid JSON should produce an error");
142 let http_error: HttpError = json_error.into();
143
144 match http_error {
145 HttpError::Serialization(msg) => {
146 assert!(
147 msg.contains("expected"),
148 "Error message should describe JSON error"
149 );
150 }
151 _ => panic!("Expected HttpError::Serialization"),
152 }
153 }
154
155 #[test]
156 fn test_replay_safe_error_classification() {
157 assert!(HttpError::Connection("reset".to_string()).is_replay_safe());
158 assert!(HttpError::Timeout.is_replay_safe());
159
160 for status in [400, 408, 429, 500, 503] {
161 assert!(!HttpError::Status {
162 status,
163 message: "request failed".to_string(),
164 }
165 .is_replay_safe());
166 }
167
168 assert!(!HttpError::Serialization("bad JSON".to_string()).is_replay_safe());
169 assert!(!HttpError::Other("attestation failed".to_string()).is_replay_safe());
170 }
171}