reqwest_extra/
lib.rs

1#![warn(clippy::pedantic)]
2#![warn(missing_docs)]
3//! Extra utilities for [`reqwest`](https://crates.io/crates/reqwest).
4
5pub use reqwest;
6
7use std::{error::Error, fmt::Display};
8
9use bytes::Bytes;
10use reqwest::Response;
11
12/// A [`reqwest::Error`] that may also contain the response body.
13///
14/// Created from a response using the
15/// [`ResponseExt::error_for_status_with_body`] method. Can also be created from
16/// a [`reqwest::Error`].
17///
18/// # Example
19///
20/// ```
21/// use reqwest_extra::{ErrorWithBody, ResponseExt};
22///
23/// async fn fetch_string(url: &str) -> Result<String, ErrorWithBody> {
24///     let response = reqwest::get(url)
25///         .await?
26///         .error_for_status_with_body()
27///         .await?
28///         .text()
29///         .await?;
30///     Ok(response)
31/// }
32///
33/// # #[tokio::main]
34/// # async fn main() {
35///     let err = fetch_string("https://api.github.com/user").await.unwrap_err();
36///     println!("{err}");
37/// # }
38/// ```
39///
40/// Output (line-wrapped for readability):
41/// ```text
42/// HTTP status client error (403 Forbidden) for url (https://api.github.com/user),
43/// body: b"\r\nRequest forbidden by administrative rules.
44/// Please make sure your request has a User-Agent header
45/// (https://docs.github.com/en/rest/overview/resources-in-the-rest-api#user-agent-required).
46/// Check https://developer.github.com for other possible causes.\r\n"
47/// ```
48#[derive(Debug)]
49pub struct ErrorWithBody {
50    inner: reqwest::Error,
51    body: Option<Result<Bytes, reqwest::Error>>,
52}
53
54impl ErrorWithBody {
55    /// Get a reference to the inner [`reqwest::Error`].
56    #[must_use]
57    pub fn inner(&self) -> &reqwest::Error {
58        &self.inner
59    }
60
61    /// Get a mutable reference to the inner [`reqwest::Error`].
62    #[must_use]
63    pub fn inner_mut(&mut self) -> &mut reqwest::Error {
64        &mut self.inner
65    }
66
67    /// Consume the `ErrorWithBody`, returning the inner [`reqwest::Error`].
68    #[must_use]
69    pub fn into_inner(self) -> reqwest::Error {
70        self.inner
71    }
72
73    /// Get a reference to the response body, if available.
74    #[must_use]
75    pub fn body(&self) -> Option<&Result<Bytes, reqwest::Error>> {
76        self.body.as_ref()
77    }
78
79    /// Get a mutable reference to the response body, if available.
80    #[must_use]
81    pub fn body_mut(&mut self) -> Option<&mut Result<Bytes, reqwest::Error>> {
82        self.body.as_mut()
83    }
84
85    /// Consume the `ErrorWithBody`, returning the response body, if available.
86    #[must_use]
87    pub fn into_body(self) -> Option<Result<Bytes, reqwest::Error>> {
88        self.body
89    }
90
91    /// Consume the `ErrorWithBody`, returning both the inner [`reqwest::Error`]
92    /// and the response body, if available.
93    #[must_use]
94    pub fn into_parts(self) -> (reqwest::Error, Option<Result<Bytes, reqwest::Error>>) {
95        (self.inner, self.body)
96    }
97
98    /// Add a url related to this error (overwriting any existing).
99    #[must_use]
100    pub fn with_url(self, url: reqwest::Url) -> Self {
101        ErrorWithBody {
102            inner: self.inner.with_url(url),
103            body: self.body,
104        }
105    }
106
107    /// Strip the related url from this error (if, for example, it contains
108    /// sensitive information).
109    #[must_use]
110    pub fn without_url(self) -> Self {
111        ErrorWithBody {
112            inner: self.inner.without_url(),
113            body: self.body,
114        }
115    }
116}
117
118impl Display for ErrorWithBody {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        write!(f, "{}", self.inner)?;
121        if let Some(body) = &self.body {
122            match body {
123                Ok(body) => {
124                    write!(f, ", body: {body:?}")?;
125                }
126                Err(body_error) => {
127                    write!(f, ", error reading body: {body_error}")?;
128                }
129            }
130        }
131        Ok(())
132    }
133}
134
135impl Error for ErrorWithBody {
136    fn source(&self) -> Option<&(dyn Error + 'static)> {
137        Some(&self.inner)
138    }
139}
140
141impl From<reqwest::Error> for ErrorWithBody {
142    fn from(err: reqwest::Error) -> Self {
143        ErrorWithBody {
144            inner: err,
145            body: None,
146        }
147    }
148}
149
150/// Extension trait for [`reqwest::Response`] to provide additional
151/// functionality.
152pub trait ResponseExt: sealed::Sealed {
153    /// Like [`reqwest::Response::error_for_status`], but if the response is an
154    /// error, also reads and includes the response body in the returned
155    /// error.
156    ///
157    /// # Example
158    /// ```
159    /// # #[tokio::main]
160    /// # async fn main() {
161    /// use reqwest_extra::ResponseExt;
162    ///
163    /// let response = reqwest::get("https://api.github.com/user").await.unwrap();
164    /// let err = response.error_for_status_with_body().await.unwrap_err();
165    /// println!("{err}");
166    /// # }
167    /// ```
168    ///
169    /// Output (line-wrapped for readability):
170    /// ```text
171    /// HTTP status client error (403 Forbidden) for url (https://api.github.com/user),
172    /// body: b"\r\nRequest forbidden by administrative rules.
173    /// Please make sure your request has a User-Agent header
174    /// (https://docs.github.com/en/rest/overview/resources-in-the-rest-api#user-agent-required).
175    /// Check https://developer.github.com for other possible causes.\r\n"
176    /// ```
177    fn error_for_status_with_body(
178        self,
179    ) -> impl Future<Output = Result<Response, ErrorWithBody>> + Send + Sync + 'static;
180}
181
182impl ResponseExt for Response {
183    async fn error_for_status_with_body(self) -> Result<Response, ErrorWithBody> {
184        match self.error_for_status_ref() {
185            Ok(_) => Ok(self),
186            Err(e) => {
187                let body = self.bytes().await;
188                Err(ErrorWithBody {
189                    inner: e,
190                    body: Some(body),
191                })
192            }
193        }
194    }
195}
196
197mod sealed {
198    pub trait Sealed {}
199    impl Sealed for reqwest::Response {}
200}