reqwest_extra/
lib.rs

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