1use std::sync::Arc;
4
5use thiserror::Error;
6
7#[derive(Debug, Clone, Error)]
9pub enum OxiHttpError {
10 #[error("invalid URI: {0}")]
12 InvalidUri(Arc<http::uri::InvalidUri>),
13
14 #[error("HTTP error: {0}")]
16 Http(Arc<http::Error>),
17
18 #[error("hyper error: {0}")]
20 Hyper(String),
21
22 #[error("I/O error: {0}")]
24 Io(Arc<std::io::Error>),
25
26 #[error("body error: {0}")]
28 Body(String),
29
30 #[error("timeout: {0}")]
32 Timeout(String),
33
34 #[error("redirect error: {0}")]
36 Redirect(String),
37
38 #[error("TLS error: {0}")]
40 Tls(String),
41
42 #[error("DNS error: {0}")]
44 Dns(String),
45
46 #[error("connection pool error: {0}")]
48 ConnectionPool(String),
49
50 #[error("JSON error: {0}")]
52 Json(String),
53
54 #[error("form encoding error: {0}")]
56 FormEncoding(String),
57
58 #[error("invalid header: {0}")]
60 InvalidHeader(String),
61
62 #[error("server error: {0}")]
64 Server(String),
65
66 #[error("route not found: {method} {path}")]
68 RouteNotFound {
69 method: String,
71 path: String,
73 },
74
75 #[error("method not allowed: {method} {path}")]
77 MethodNotAllowed {
78 method: String,
80 path: String,
82 },
83
84 #[error("HTTP/3 error: {0}")]
86 H3(String),
87}
88
89impl From<http::uri::InvalidUri> for OxiHttpError {
90 fn from(e: http::uri::InvalidUri) -> Self {
91 OxiHttpError::InvalidUri(Arc::new(e))
92 }
93}
94
95impl From<std::io::Error> for OxiHttpError {
96 fn from(e: std::io::Error) -> Self {
97 OxiHttpError::Io(Arc::new(e))
98 }
99}
100
101impl From<http::Error> for OxiHttpError {
102 fn from(e: http::Error) -> Self {
103 OxiHttpError::Http(Arc::new(e))
104 }
105}
106
107#[cfg(feature = "tls")]
108impl From<oxitls_core::TlsError> for OxiHttpError {
109 fn from(e: oxitls_core::TlsError) -> Self {
110 OxiHttpError::Tls(e.to_string())
111 }
112}
113
114impl OxiHttpError {
115 pub fn status_code(&self) -> Option<http::StatusCode> {
117 match self {
118 Self::RouteNotFound { .. } => Some(http::StatusCode::NOT_FOUND),
119 Self::MethodNotAllowed { .. } => Some(http::StatusCode::METHOD_NOT_ALLOWED),
120 Self::Timeout(_) => Some(http::StatusCode::REQUEST_TIMEOUT),
121 _ => None,
122 }
123 }
124
125 pub fn is_timeout(&self) -> bool {
127 matches!(self, Self::Timeout(_))
128 }
129
130 pub fn is_connect(&self) -> bool {
132 matches!(self, Self::Dns(_) | Self::ConnectionPool(_) | Self::Tls(_))
133 }
134
135 pub fn is_body(&self) -> bool {
137 matches!(self, Self::Body(_))
138 }
139
140 pub fn is_redirect(&self) -> bool {
142 matches!(self, Self::Redirect(_))
143 }
144}
145
146#[cfg(test)]
147mod clone_tests {
148 use super::*;
149
150 #[test]
151 fn test_oxi_http_error_is_clone() {
152 let io_err = OxiHttpError::from(std::io::Error::new(std::io::ErrorKind::NotFound, "test"));
153 let cloned = io_err.clone();
154 assert_eq!(io_err.to_string(), cloned.to_string());
155
156 let str_err = OxiHttpError::Body("test".to_string());
157 let _ = str_err.clone();
158 }
159}
160
161#[cfg(test)]
162mod error_tests {
163 use super::*;
164
165 #[test]
170 fn test_display_invalid_uri() {
171 let raw_err: http::uri::InvalidUri = "not a valid uri!!!"
172 .parse::<http::Uri>()
173 .expect_err("should fail to parse");
174 let err = OxiHttpError::from(raw_err);
175 let msg = err.to_string();
176 assert!(
177 msg.contains("invalid URI"),
178 "expected 'invalid URI' in '{msg}'"
179 );
180 }
181
182 #[test]
183 fn test_display_http_error() {
184 let raw_err = http::Request::builder()
185 .header("\n", "x")
186 .body(())
187 .expect_err("should fail with invalid header name");
188 let err = OxiHttpError::from(raw_err);
189 let msg = err.to_string();
190 assert!(
191 msg.contains("HTTP error"),
192 "expected 'HTTP error' in '{msg}'"
193 );
194 }
195
196 #[test]
197 fn test_display_hyper_error() {
198 let err = OxiHttpError::Hyper("connection reset".to_string());
199 let msg = err.to_string();
200 assert!(
201 msg.contains("hyper error"),
202 "expected 'hyper error' in '{msg}'"
203 );
204 }
205
206 #[test]
207 fn test_display_io_error() {
208 let raw_err = std::io::Error::new(
209 std::io::ErrorKind::ConnectionRefused,
210 "connection refused test",
211 );
212 let err = OxiHttpError::from(raw_err);
213 let msg = err.to_string();
214 assert!(msg.contains("I/O error"), "expected 'I/O error' in '{msg}'");
215 }
216
217 #[test]
218 fn test_display_body_error() {
219 let err = OxiHttpError::Body("chunk too large".to_string());
220 let msg = err.to_string();
221 assert!(
222 msg.contains("body error"),
223 "expected 'body error' in '{msg}'"
224 );
225 }
226
227 #[test]
228 fn test_display_timeout() {
229 let err = OxiHttpError::Timeout("request timed out".to_string());
230 let msg = err.to_string();
231 assert!(msg.contains("timeout"), "expected 'timeout' in '{msg}'");
232 }
233
234 #[test]
235 fn test_display_redirect() {
236 let err = OxiHttpError::Redirect("too many redirects".to_string());
237 let msg = err.to_string();
238 assert!(
239 msg.contains("redirect error"),
240 "expected 'redirect error' in '{msg}'"
241 );
242 }
243
244 #[test]
245 fn test_display_tls() {
246 let err = OxiHttpError::Tls("certificate invalid".to_string());
247 let msg = err.to_string();
248 assert!(msg.contains("TLS error"), "expected 'TLS error' in '{msg}'");
249 }
250
251 #[test]
252 fn test_display_dns() {
253 let err = OxiHttpError::Dns("no such host".to_string());
254 let msg = err.to_string();
255 assert!(msg.contains("DNS error"), "expected 'DNS error' in '{msg}'");
256 }
257
258 #[test]
259 fn test_display_connection_pool() {
260 let err = OxiHttpError::ConnectionPool("pool exhausted".to_string());
261 let msg = err.to_string();
262 assert!(
263 msg.contains("connection pool error"),
264 "expected 'connection pool error' in '{msg}'"
265 );
266 }
267
268 #[test]
269 fn test_display_json() {
270 let err = OxiHttpError::Json("unexpected token".to_string());
271 let msg = err.to_string();
272 assert!(
273 msg.contains("JSON error"),
274 "expected 'JSON error' in '{msg}'"
275 );
276 }
277
278 #[test]
279 fn test_display_route_not_found() {
280 let err = OxiHttpError::RouteNotFound {
281 method: "GET".to_string(),
282 path: "/foo".to_string(),
283 };
284 let msg = err.to_string();
285 assert!(
286 msg.contains("route not found"),
287 "expected 'route not found' in '{msg}'"
288 );
289 assert!(msg.contains("GET"), "expected 'GET' in '{msg}'");
290 assert!(msg.contains("/foo"), "expected '/foo' in '{msg}'");
291 }
292
293 #[test]
294 fn test_display_method_not_allowed() {
295 let err = OxiHttpError::MethodNotAllowed {
296 method: "DELETE".to_string(),
297 path: "/bar".to_string(),
298 };
299 let msg = err.to_string();
300 assert!(
301 msg.contains("method not allowed"),
302 "expected 'method not allowed' in '{msg}'"
303 );
304 assert!(msg.contains("DELETE"), "expected 'DELETE' in '{msg}'");
305 assert!(msg.contains("/bar"), "expected '/bar' in '{msg}'");
306 }
307
308 #[test]
313 fn test_from_invalid_uri() {
314 let raw: http::uri::InvalidUri = "not a valid uri!!!"
315 .parse::<http::Uri>()
316 .expect_err("should fail");
317 let result = OxiHttpError::from(raw);
318 assert!(
319 matches!(result, OxiHttpError::InvalidUri(_)),
320 "expected InvalidUri variant"
321 );
322 }
323
324 #[test]
325 fn test_from_http_error() {
326 let raw = http::Request::builder()
327 .header("\n", "x")
328 .body(())
329 .expect_err("should fail with invalid header name");
330 let result = OxiHttpError::from(raw);
331 assert!(
332 matches!(result, OxiHttpError::Http(_)),
333 "expected Http variant"
334 );
335 }
336
337 #[test]
338 fn test_from_io_error() {
339 let raw = std::io::Error::new(
340 std::io::ErrorKind::ConnectionRefused,
341 "test io error message",
342 );
343 let result = OxiHttpError::from(raw);
344 assert!(matches!(result, OxiHttpError::Io(_)), "expected Io variant");
345 assert!(
346 result.to_string().contains("test io error message"),
347 "Display should include the original io message"
348 );
349 }
350
351 #[test]
356 fn test_status_code_route_not_found() {
357 let err = OxiHttpError::RouteNotFound {
358 method: "GET".to_string(),
359 path: "/missing".to_string(),
360 };
361 assert_eq!(err.status_code(), Some(http::StatusCode::NOT_FOUND));
362 }
363
364 #[test]
365 fn test_status_code_method_not_allowed() {
366 let err = OxiHttpError::MethodNotAllowed {
367 method: "PUT".to_string(),
368 path: "/resource".to_string(),
369 };
370 assert_eq!(
371 err.status_code(),
372 Some(http::StatusCode::METHOD_NOT_ALLOWED)
373 );
374 }
375
376 #[test]
377 fn test_status_code_timeout() {
378 let err = OxiHttpError::Timeout("waited too long".to_string());
379 assert_eq!(err.status_code(), Some(http::StatusCode::REQUEST_TIMEOUT));
380 }
381
382 #[test]
383 fn test_status_code_body_is_none() {
384 let err = OxiHttpError::Body("incomplete body".to_string());
385 assert_eq!(err.status_code(), None);
386 }
387
388 #[test]
393 fn test_is_timeout_true() {
394 let err = OxiHttpError::Timeout("timed out".to_string());
395 assert!(err.is_timeout());
396 }
397
398 #[test]
399 fn test_is_timeout_false() {
400 let err = OxiHttpError::Body("body error".to_string());
401 assert!(!err.is_timeout());
402 }
403
404 #[test]
405 fn test_is_connect_dns() {
406 let err = OxiHttpError::Dns("nxdomain".to_string());
407 assert!(err.is_connect());
408 }
409
410 #[test]
411 fn test_is_connect_pool() {
412 let err = OxiHttpError::ConnectionPool("exhausted".to_string());
413 assert!(err.is_connect());
414 }
415
416 #[test]
417 fn test_is_connect_tls() {
418 let err = OxiHttpError::Tls("bad cert".to_string());
419 assert!(err.is_connect());
420 }
421
422 #[test]
423 fn test_is_connect_false() {
424 let err = OxiHttpError::Timeout("timed out".to_string());
425 assert!(!err.is_connect());
426 }
427
428 #[test]
429 fn test_is_body_true() {
430 let err = OxiHttpError::Body("truncated".to_string());
431 assert!(err.is_body());
432 }
433
434 #[test]
435 fn test_is_body_false() {
436 let err = OxiHttpError::Json("bad json".to_string());
437 assert!(!err.is_body());
438 }
439
440 #[test]
441 fn test_is_redirect_true() {
442 let err = OxiHttpError::Redirect("loop detected".to_string());
443 assert!(err.is_redirect());
444 }
445
446 #[test]
447 fn test_is_redirect_false() {
448 let err = OxiHttpError::Timeout("timed out".to_string());
449 assert!(!err.is_redirect());
450 }
451}