Skip to main content

better_fetch/
response.rs

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