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
120impl From<hyper::Error> for HttpError {
121 fn from(err: hyper::Error) -> Self {
122 HttpError::Transport(Box::new(err))
123 }
124}
125
126impl From<hyper_util::client::legacy::Error> for HttpError {
127 fn from(err: hyper_util::client::legacy::Error) -> Self {
128 HttpError::Transport(Box::new(err))
129 }
130}
131
132#[cfg(test)]
133#[cfg_attr(coverage_nightly, coverage(off))]
134mod tests {
135 use super::*;
136 use std::error::Error;
137 use std::fmt;
138
139 #[derive(Debug)]
140 struct TestError(&'static str);
141
142 impl fmt::Display for TestError {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 write!(f, "{}", self.0)
145 }
146 }
147
148 impl Error for TestError {}
149
150 #[test]
151 fn test_transport_error_preserves_source() {
152 let inner = TestError("connection refused");
153 let err = HttpError::Transport(Box::new(inner));
154
155 let source = err.source();
157 assert!(source.is_some(), "Transport error should have a source");
158
159 let source = source.unwrap();
161 let downcast = source.downcast_ref::<TestError>();
162 assert!(
163 downcast.is_some(),
164 "Should be able to downcast to TestError"
165 );
166 assert_eq!(downcast.unwrap().0, "connection refused");
167 }
168
169 #[test]
170 fn test_tls_error_preserves_source() {
171 let inner = TestError("certificate expired");
172 let err = HttpError::Tls(Box::new(inner));
173
174 let source = err.source();
175 assert!(source.is_some(), "TLS error should have a source");
176
177 let source = source.unwrap();
178 let downcast = source.downcast_ref::<TestError>();
179 assert!(downcast.is_some());
180 assert_eq!(downcast.unwrap().0, "certificate expired");
181 }
182
183 #[test]
184 fn test_error_chain_traversal() {
185 let inner = TestError("root cause");
186 let err = HttpError::Transport(Box::new(inner));
187
188 let mut count = 0;
190 let mut current: Option<&(dyn Error + 'static)> = Some(&err);
191 while let Some(e) = current {
192 count += 1;
193 current = e.source();
194 }
195
196 assert_eq!(
197 count, 2,
198 "Should have 2 errors in chain: HttpError and TestError"
199 );
200 }
201}