Skip to main content

better_fetch/
response.rs

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