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}