Skip to main content

cdk_http_client/
error.rs

1//! HTTP error types
2
3use thiserror::Error;
4
5/// HTTP errors that can occur during requests
6#[derive(Debug, Error)]
7pub enum HttpError {
8    /// HTTP error with status code
9    #[error("HTTP error ({status}): {message}")]
10    Status {
11        /// HTTP status code
12        status: u16,
13        /// Error message
14        message: String,
15    },
16    /// Connection error
17    #[error("Connection error: {0}")]
18    Connection(String),
19    /// Request timeout
20    #[error("Request timeout")]
21    Timeout,
22    /// Serialization error
23    #[error("Serialization error: {0}")]
24    Serialization(String),
25    /// Proxy error
26    #[error("Proxy error: {0}")]
27    Proxy(String),
28    /// Client build error
29    #[error("Client build error: {0}")]
30    Build(String),
31    /// Other error
32    #[error("{0}")]
33    Other(String),
34}
35
36impl HttpError {
37    /// Return whether this error is eligible for NUT-19 request replay.
38    ///
39    /// Only ambiguous transport failures are eligible. HTTP status responses
40    /// are never eligible because NUT-19 caches successful responses only; a
41    /// failed status therefore does not prove that replaying a side-effecting
42    /// request is safe.
43    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        // Create an invalid JSON parse to get a serde_json::Error
140        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}