1use std::time::Duration;
2use thiserror::Error;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum InvalidUriKind {
25 ParseError,
27 MissingAuthority,
29 MissingScheme,
31}
32
33#[derive(Error, Debug)]
35#[non_exhaustive]
36pub enum HttpError {
37 #[error("Failed to build request: {0}")]
39 RequestBuild(#[from] http::Error),
40
41 #[error("Invalid header name: {0}")]
43 InvalidHeaderName(#[from] http::header::InvalidHeaderName),
44
45 #[error("Invalid header value: {0}")]
47 InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
48
49 #[error("Request attempt timed out after {0:?}")]
51 Timeout(std::time::Duration),
52
53 #[error("Operation deadline exceeded after {0:?}")]
55 DeadlineExceeded(std::time::Duration),
56
57 #[error("Transport error: {0}")]
59 Transport(#[source] Box<dyn std::error::Error + Send + Sync>),
60
61 #[error("TLS error: {0}")]
63 Tls(#[source] Box<dyn std::error::Error + Send + Sync>),
64
65 #[error("Response body too large: limit {limit} bytes, got {actual} bytes")]
67 BodyTooLarge { limit: usize, actual: usize },
68
69 #[error("HTTP {status}: {body_preview}")]
71 HttpStatus {
72 status: http::StatusCode,
73 body_preview: String,
74 content_type: Option<String>,
75 retry_after: Option<Duration>,
77 },
78
79 #[error("JSON parsing failed: {0}")]
81 Json(#[from] serde_json::Error),
82
83 #[error("Form encoding failed: {0}")]
85 FormEncode(#[from] serde_urlencoded::ser::Error),
86
87 #[error("Service overloaded: concurrency limit reached")]
89 Overloaded,
90
91 #[error("Service unavailable: internal failure")]
93 ServiceClosed,
94
95 #[error("Invalid URL '{url}': {reason}")]
101 InvalidUri {
102 url: String,
104 kind: InvalidUriKind,
106 reason: String,
108 },
109
110 #[error("URL scheme '{scheme}' not allowed: {reason}")]
112 InvalidScheme {
113 scheme: String,
115 reason: String,
117 },
118
119 #[error("insecure transport (AllowInsecureHttp) is not permitted under FIPS")]
126 InsecureTransport,
127}
128
129impl From<hyper::Error> for HttpError {
130 fn from(err: hyper::Error) -> Self {
131 HttpError::Transport(Box::new(err))
132 }
133}
134
135impl From<hyper_util::client::legacy::Error> for HttpError {
136 fn from(err: hyper_util::client::legacy::Error) -> Self {
137 HttpError::Transport(Box::new(err))
138 }
139}
140
141#[cfg(test)]
142#[cfg_attr(coverage_nightly, coverage(off))]
143mod tests {
144 use super::*;
145 use std::error::Error;
146 use std::fmt;
147
148 #[derive(Debug)]
149 struct TestError(&'static str);
150
151 impl fmt::Display for TestError {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 write!(f, "{}", self.0)
154 }
155 }
156
157 impl Error for TestError {}
158
159 #[test]
160 fn test_transport_error_preserves_source() {
161 let inner = TestError("connection refused");
162 let err = HttpError::Transport(Box::new(inner));
163
164 let source = err.source();
166 assert!(source.is_some(), "Transport error should have a source");
167
168 let source = source.unwrap();
170 let downcast = source.downcast_ref::<TestError>();
171 assert!(
172 downcast.is_some(),
173 "Should be able to downcast to TestError"
174 );
175 assert_eq!(downcast.unwrap().0, "connection refused");
176 }
177
178 #[test]
179 fn test_tls_error_preserves_source() {
180 let inner = TestError("certificate expired");
181 let err = HttpError::Tls(Box::new(inner));
182
183 let source = err.source();
184 assert!(source.is_some(), "TLS error should have a source");
185
186 let source = source.unwrap();
187 let downcast = source.downcast_ref::<TestError>();
188 assert!(downcast.is_some());
189 assert_eq!(downcast.unwrap().0, "certificate expired");
190 }
191
192 #[test]
193 fn test_error_chain_traversal() {
194 let inner = TestError("root cause");
195 let err = HttpError::Transport(Box::new(inner));
196
197 let mut count = 0;
199 let mut current: Option<&(dyn Error + 'static)> = Some(&err);
200 while let Some(e) = current {
201 count += 1;
202 current = e.source();
203 }
204
205 assert_eq!(
206 count, 2,
207 "Should have 2 errors in chain: HttpError and TestError"
208 );
209 }
210}