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}