Skip to main content

better_fetch/
response.rs

1use bytes::Bytes;
2use http::{HeaderMap, StatusCode};
3
4use crate::error::Error;
5use crate::Result;
6
7/// HTTP response wrapper.
8///
9/// The full body is already buffered in memory as [`Bytes`] when you receive this type.
10/// Methods named `into_*` perform parsing synchronously; the `async` counterparts
11/// (`text`, `json`, …) delegate to `into_*` without additional I/O and exist for
12/// ergonomics in async code (e.g. [`RequestBuilder::send_json`](crate::request::RequestBuilder::send_json)).
13/// Prefer `into_json`, `into_text`, and `into_bytes_checked` on hot paths.
14///
15/// This model suits typical JSON APIs. It does not stream large downloads; use reqwest
16/// directly when you need chunked bodies or custom size limits.
17#[derive(Clone)]
18pub struct Response {
19    status: StatusCode,
20    headers: HeaderMap,
21    body: Bytes,
22    url: Option<url::Url>,
23    #[cfg(feature = "json")]
24    json_parser: Option<crate::json_parser::JsonParserFn>,
25}
26
27impl Response {
28    pub(crate) fn new(
29        status: StatusCode,
30        headers: HeaderMap,
31        body: Bytes,
32        url: Option<url::Url>,
33        #[cfg(feature = "json")] json_parser: Option<crate::json_parser::JsonParserFn>,
34    ) -> Self {
35        Self {
36            status,
37            headers,
38            body,
39            url,
40            #[cfg(feature = "json")]
41            json_parser,
42        }
43    }
44
45    pub fn status(&self) -> StatusCode {
46        self.status
47    }
48
49    pub fn headers(&self) -> &HeaderMap {
50        &self.headers
51    }
52
53    /// Returns a reference to the fully buffered response body.
54    pub fn bytes(&self) -> &Bytes {
55        &self.body
56    }
57
58    pub fn url(&self) -> Option<&url::Url> {
59        self.url.as_ref()
60    }
61
62    pub fn is_success(&self) -> bool {
63        self.status.is_success()
64    }
65
66    /// Returns an error if the status is not success.
67    #[must_use = "call `?` or handle the error explicitly"]
68    pub fn error_for_status(&self) -> Result<()> {
69        if self.status.is_success() {
70            return Ok(());
71        }
72        Err(Error::http_with_status_text(
73            self.status,
74            self.status.canonical_reason().unwrap_or("request failed"),
75            self.status.canonical_reason().unwrap_or("request failed"),
76            Some(self.body.clone()),
77        ))
78    }
79
80    /// Reads the body as UTF-8 after checking for a success status.
81    ///
82    /// Prefer this over [`text`](Self::text) when you do not need an `.await` (no extra I/O).
83    pub fn into_text(self) -> Result<String> {
84        self.error_for_status()?;
85        Ok(String::from_utf8_lossy(&self.body).into_owned())
86    }
87
88    /// Async alias for [`into_text`](Self::into_text); does not perform additional I/O.
89    pub async fn text(self) -> Result<String> {
90        self.into_text()
91    }
92
93    /// Returns the body after checking for a success status.
94    ///
95    /// Prefer this over [`bytes_checked`](Self::bytes_checked) on hot paths.
96    pub fn into_bytes_checked(self) -> Result<Bytes> {
97        self.error_for_status()?;
98        Ok(self.body)
99    }
100
101    /// Async alias for [`into_bytes_checked`](Self::into_bytes_checked).
102    pub async fn bytes_checked(self) -> Result<Bytes> {
103        self.into_bytes_checked()
104    }
105
106    /// Deserializes JSON after checking for a success status, using the client or request
107    /// [`JsonParserFn`](crate::json_parser::JsonParserFn) when configured.
108    ///
109    /// Prefer this over [`json`](Self::json) on hot paths. See [`crate::json_parser`] for the
110    /// default single-step path vs a custom two-step parser.
111    #[cfg(feature = "json")]
112    pub fn into_json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
113        self.error_for_status()?;
114        crate::json_parser::deserialize(&self.body, self.status, self.json_parser.as_ref())
115    }
116
117    /// Async alias for [`into_json`](Self::into_json).
118    #[cfg(feature = "json")]
119    pub async fn json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
120        self.into_json()
121    }
122
123    /// Deserializes JSON in one step with a custom closure (`Bytes` → `T`).
124    ///
125    /// Ignores any client- or request-level [`JsonParserFn`](crate::json_parser::JsonParserFn).
126    /// Use this for BOM stripping or other transforms without the `Value` intermediate
127    /// required by [`ClientBuilder::json_parser`](crate::client::ClientBuilder::json_parser).
128    #[cfg(feature = "json")]
129    pub fn into_json_with<T, F>(self, parse: F) -> Result<T>
130    where
131        T: serde::de::DeserializeOwned,
132        F: FnOnce(&Bytes) -> std::result::Result<T, String>,
133    {
134        self.error_for_status()?;
135        parse(&self.body).map_err(|message| {
136            crate::json_parser::deserialize_error(self.status, message, &self.body)
137        })
138    }
139
140    /// Async alias for [`into_json_with`](Self::into_json_with).
141    #[cfg(feature = "json")]
142    pub async fn json_with<T, F>(self, parse: F) -> Result<T>
143    where
144        T: serde::de::DeserializeOwned,
145        F: FnOnce(&Bytes) -> std::result::Result<T, String>,
146    {
147        self.into_json_with(parse)
148    }
149
150    /// Deserializes JSON without checking HTTP status, using the configured [`JsonParserFn`](crate::json_parser::JsonParserFn) when set.
151    #[cfg(feature = "json")]
152    pub fn into_json_unchecked<T: serde::de::DeserializeOwned>(self) -> Result<T> {
153        crate::json_parser::deserialize(&self.body, self.status, self.json_parser.as_ref())
154    }
155
156    /// Async alias for [`into_json_unchecked`](Self::into_json_unchecked).
157    #[cfg(feature = "json")]
158    pub async fn json_unchecked<T: serde::de::DeserializeOwned>(self) -> Result<T> {
159        self.into_json_unchecked()
160    }
161
162    /// Deserialize JSON and run [`garde::Validate`] rules (feature `validate`).
163    #[cfg(feature = "validate")]
164    pub fn into_json_validated<T>(self) -> Result<T>
165    where
166        T: serde::de::DeserializeOwned + garde::Validate,
167        T::Context: Default,
168    {
169        self.error_for_status()?;
170        crate::validate_json::deserialize_validated(
171            &self.body,
172            self.status,
173            self.json_parser.as_ref(),
174        )
175    }
176
177    /// Deserialize JSON and run [`garde::Validate`] rules (feature `validate`).
178    #[cfg(feature = "validate")]
179    pub async fn json_validated<T>(self) -> Result<T>
180    where
181        T: serde::de::DeserializeOwned + garde::Validate,
182        T::Context: Default,
183    {
184        self.into_json_validated()
185    }
186
187    /// Like [`into_json_validated`](Self::into_json_validated) without checking HTTP status.
188    #[cfg(feature = "validate")]
189    pub fn into_json_validated_unchecked<T>(self) -> Result<T>
190    where
191        T: serde::de::DeserializeOwned + garde::Validate,
192        T::Context: Default,
193    {
194        crate::validate_json::deserialize_validated(
195            &self.body,
196            self.status,
197            self.json_parser.as_ref(),
198        )
199    }
200
201    /// Like [`into_json_validated`](Self::into_json_validated) without checking HTTP status.
202    #[cfg(feature = "validate")]
203    pub async fn json_validated_unchecked<T>(self) -> Result<T>
204    where
205        T: serde::de::DeserializeOwned + garde::Validate,
206        T::Context: Default,
207    {
208        self.into_json_validated_unchecked()
209    }
210
211    pub fn into_parts(self) -> (StatusCode, HeaderMap, Bytes) {
212        (self.status, self.headers, self.body)
213    }
214}
215
216impl std::fmt::Debug for Response {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        let mut debug = f.debug_struct("Response");
219        debug
220            .field("status", &self.status)
221            .field("headers", &self.headers)
222            .field("body", &self.body)
223            .field("url", &self.url);
224        #[cfg(feature = "json")]
225        if self.json_parser.is_some() {
226            debug.field("json_parser", &"<custom>");
227        }
228        debug.finish()
229    }
230}
231
232#[cfg(all(test, feature = "json"))]
233mod tests {
234    use super::*;
235    use serde::Deserialize;
236
237    #[derive(Debug, Deserialize, PartialEq)]
238    struct IdOnly {
239        id: u64,
240    }
241
242    #[test]
243    fn into_text_returns_body_on_success() {
244        let response = Response::new(
245            StatusCode::OK,
246            HeaderMap::new(),
247            Bytes::from_static(b"hello"),
248            None,
249            None,
250        );
251        assert_eq!(response.into_text().unwrap(), "hello");
252    }
253
254    #[test]
255    fn into_json_deserializes_without_async() {
256        let response = Response::new(
257            StatusCode::OK,
258            HeaderMap::new(),
259            Bytes::from_static(br#"{"id":7}"#),
260            None,
261            None,
262        );
263        assert_eq!(response.into_json::<IdOnly>().unwrap(), IdOnly { id: 7 });
264    }
265
266    #[test]
267    fn into_json_with_strips_bom_without_client_parser() {
268        let response = Response::new(
269            StatusCode::OK,
270            HeaderMap::new(),
271            Bytes::from_static(b"\xef\xbb\xbf{\"id\":3}"),
272            None,
273            None,
274        );
275        let parsed: IdOnly = response
276            .into_json_with(|body| {
277                let slice = body.strip_prefix(b"\xef\xbb\xbf").unwrap_or(body);
278                serde_json::from_slice(slice).map_err(|e| e.to_string())
279            })
280            .unwrap();
281        assert_eq!(parsed, IdOnly { id: 3 });
282    }
283}