fast_down/reqwest/
error.rs1use reqwest::Response;
9use url::Url;
10
11#[derive(thiserror::Error, Debug)]
13pub enum ReqwestGetHeaderError {
14 #[error("Header not found")]
16 NotFound,
17}
18
19#[derive(Debug, thiserror::Error)]
26#[error(transparent)]
27pub struct BuildClientError(#[from] reqwest::Error);
28
29impl std::ops::Deref for BuildClientError {
30 type Target = reqwest::Error;
31 fn deref(&self) -> &Self::Target {
32 &self.0
33 }
34}
35
36#[derive(Debug, thiserror::Error)]
42#[error(transparent)]
43pub struct RequestError(#[from] reqwest::Error);
44
45impl std::ops::Deref for RequestError {
46 type Target = reqwest::Error;
47 fn deref(&self) -> &Self::Target {
48 &self.0
49 }
50}
51
52#[derive(Debug, thiserror::Error)]
64#[error(
65 "Url: {}, Status Code: {}, Headers: {:?}",
66 .resp.url(),
67 .resp.status().as_u16(),
68 .resp.headers()
69)]
70pub struct StatusCodeError {
71 resp: Response,
72}
73
74pub const MAX_ERROR_BODY: usize = 64 * 1024;
76
77impl StatusCodeError {
78 pub(crate) const fn from_response(resp: Response) -> Self {
80 Self { resp }
81 }
82
83 #[must_use]
85 pub fn status(&self) -> u16 {
86 self.resp.status().as_u16()
87 }
88
89 #[must_use]
91 pub fn url(&self) -> &Url {
92 self.resp.url()
93 }
94
95 #[must_use]
97 pub fn header(&self, name: &str) -> Option<&str> {
98 self.resp.headers().get(name).and_then(|v| v.to_str().ok())
99 }
100
101 #[must_use]
111 pub fn into_response(self) -> Response {
112 self.resp
113 }
114
115 pub async fn body(mut self) -> Vec<u8> {
121 let mut body = Vec::new();
122 while body.len() < MAX_ERROR_BODY {
123 match self.resp.chunk().await {
124 Ok(Some(chunk)) => {
125 let remaining = MAX_ERROR_BODY - body.len();
126 body.extend_from_slice(&chunk[..chunk.len().min(remaining)]);
127 }
128 Ok(None) | Err(_) => break,
129 }
130 }
131 body
132 }
133
134 pub async fn body_text(self) -> String {
136 String::from_utf8_lossy(&self.body().await).into_owned()
137 }
138}
139
140#[derive(thiserror::Error, Debug)]
142#[non_exhaustive]
143pub enum ReqwestResponseError {
144 #[error(transparent)]
146 Request(RequestError),
147 #[error(transparent)]
149 StatusCode(Box<StatusCodeError>),
150}
151
152impl ReqwestResponseError {
153 pub(crate) fn from_response(resp: Response) -> Self {
155 Self::StatusCode(Box::new(StatusCodeError::from_response(resp)))
156 }
157
158 #[must_use]
161 pub fn status_code(&self) -> Option<u16> {
162 match self {
163 Self::Request(_) => None,
164 Self::StatusCode(resp) => Some(resp.status()),
165 }
166 }
167
168 #[must_use]
170 pub fn url(&self) -> Option<&Url> {
171 match self {
172 Self::Request(_) => None,
173 Self::StatusCode(resp) => Some(resp.url()),
174 }
175 }
176
177 #[must_use]
179 pub fn is_timeout(&self) -> bool {
180 match self {
181 Self::Request(e) => e.is_timeout(),
182 Self::StatusCode(_) => false,
183 }
184 }
185
186 #[must_use]
189 pub fn is_connect(&self) -> bool {
190 match self {
191 Self::Request(e) => e.is_connect(),
192 Self::StatusCode(_) => false,
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 #![allow(
200 clippy::unwrap_used,
201 clippy::expect_used,
202 clippy::panic,
203 clippy::significant_drop_tightening
204 )]
205 use super::*;
206 use crate::http::Prefetch;
207 use reqwest::{Client, StatusCode};
208
209 #[tokio::test]
210 async fn test_error_handling() {
211 let mut server = mockito::Server::new_async().await;
212 let client = Client::builder().no_proxy().build().unwrap();
213 let _mock1 = server
214 .mock("GET", "/404")
215 .with_status(404)
216 .create_async()
217 .await;
218
219 let url = Url::parse(&format!("{}/404", server.url())).unwrap();
220 match client.prefetch(url).await {
221 Ok(info) => unreachable!("404 status code should not success: {info:?}"),
222 Err((err, _)) => match err {
223 ReqwestResponseError::Request(error) => unreachable!("{error:?}"),
224 ReqwestResponseError::StatusCode(resp) => {
225 assert_eq!(resp.status(), StatusCode::NOT_FOUND.as_u16());
226 }
227 },
228 }
229 }
230
231 #[tokio::test]
232 async fn status_code_error_exposes_headers_and_body() {
233 let mut server = mockito::Server::new_async().await;
237 let client = Client::builder().no_proxy().build().unwrap();
238 let _mock = server
239 .mock("GET", "/boom")
240 .with_status(502)
241 .with_header("x-reason", "upstream down")
242 .with_body("bad gateway")
243 .create_async()
244 .await;
245
246 let url = Url::parse(&format!("{}/boom", server.url())).unwrap();
247 let (err, _) = client.prefetch(url).await.expect_err("502 must fail");
248 assert_eq!(err.status_code(), Some(502));
249 assert_eq!(err.url().unwrap().path(), "/boom");
250 match err {
251 ReqwestResponseError::StatusCode(resp) => {
252 assert_eq!(resp.status(), 502);
253 assert_eq!(resp.header("x-reason"), Some("upstream down"));
254 assert_eq!(resp.body_text().await, "bad gateway");
255 }
256 ReqwestResponseError::Request(e) => unreachable!("{e:?}"),
257 }
258 }
259
260 #[tokio::test]
261 async fn status_code_error_into_response_escape_hatch() {
262 let mut server = mockito::Server::new_async().await;
265 let client = Client::builder().no_proxy().build().unwrap();
266 let _mock = server
267 .mock("GET", "/raw")
268 .with_status(503)
269 .with_body("unavailable")
270 .create_async()
271 .await;
272
273 let url = Url::parse(&format!("{}/raw", server.url())).unwrap();
274 let (err, _) = client.prefetch(url).await.expect_err("503 must fail");
275 let ReqwestResponseError::StatusCode(resp) = err else {
276 unreachable!("expected a status code error")
277 };
278 let raw = resp.into_response();
279 assert_eq!(raw.status(), StatusCode::SERVICE_UNAVAILABLE);
280 assert_eq!(raw.text().await.unwrap(), "unavailable");
281 }
282
283 #[tokio::test]
284 async fn status_code_error_caps_body() {
285 let mut server = mockito::Server::new_async().await;
288 let client = Client::builder().no_proxy().build().unwrap();
289 let payload = vec![b'x'; MAX_ERROR_BODY + 4096];
290 let _mock = server
291 .mock("GET", "/big")
292 .with_status(500)
293 .with_body(payload)
294 .create_async()
295 .await;
296
297 let url = Url::parse(&format!("{}/big", server.url())).unwrap();
298 let (err, _) = client.prefetch(url).await.expect_err("500 must fail");
299 match err {
300 ReqwestResponseError::StatusCode(resp) => {
301 assert_eq!(resp.body().await.len(), MAX_ERROR_BODY);
302 }
303 ReqwestResponseError::Request(e) => unreachable!("{e:?}"),
304 }
305 }
306
307 #[tokio::test]
308 async fn request_error_classifies_transport_failure() {
309 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
312 let addr = listener.local_addr().unwrap();
313 tokio::spawn(async move {
314 loop {
315 let (stream, _) = listener.accept().await.unwrap();
316 drop(stream);
317 }
318 });
319 let client = Client::builder().no_proxy().build().unwrap();
320 let url = Url::parse(&format!("http://{addr}/x")).unwrap();
321 let (err, _) = client
322 .prefetch(url)
323 .await
324 .expect_err("dropped conn must fail");
325 match err {
326 ReqwestResponseError::Request(e) => {
327 assert!(!e.is_timeout());
328 assert!(e.is_connect() || e.is_request());
329 assert_eq!(e.status(), None);
330 }
331 ReqwestResponseError::StatusCode(resp) => unreachable!("{}", resp.status()),
332 }
333 }
334}