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#[derive(Clone)]
9pub struct Response {
10    status: StatusCode,
11    headers: HeaderMap,
12    body: Bytes,
13    url: Option<url::Url>,
14    #[cfg(feature = "json")]
15    json_parser: Option<crate::json_parser::JsonParserFn>,
16}
17
18impl Response {
19    pub(crate) fn new(
20        status: StatusCode,
21        headers: HeaderMap,
22        body: Bytes,
23        url: Option<url::Url>,
24        #[cfg(feature = "json")] json_parser: Option<crate::json_parser::JsonParserFn>,
25    ) -> Self {
26        Self {
27            status,
28            headers,
29            body,
30            url,
31            #[cfg(feature = "json")]
32            json_parser,
33        }
34    }
35
36    pub fn status(&self) -> StatusCode {
37        self.status
38    }
39
40    pub fn headers(&self) -> &HeaderMap {
41        &self.headers
42    }
43
44    pub fn bytes(&self) -> &Bytes {
45        &self.body
46    }
47
48    pub fn url(&self) -> Option<&url::Url> {
49        self.url.as_ref()
50    }
51
52    pub fn is_success(&self) -> bool {
53        self.status.is_success()
54    }
55
56    /// Returns an error if the status is not success.
57    #[must_use = "call `?` or handle the error explicitly"]
58    pub fn error_for_status(&self) -> Result<()> {
59        if self.status.is_success() {
60            return Ok(());
61        }
62        Err(Error::http_with_status_text(
63            self.status,
64            self.status.canonical_reason().unwrap_or("request failed"),
65            self.status.canonical_reason().unwrap_or("request failed"),
66            Some(self.body.clone()),
67        ))
68    }
69
70    /// Reads the body as UTF-8 after checking for a success status.
71    pub fn into_text(self) -> Result<String> {
72        self.error_for_status()?;
73        Ok(String::from_utf8_lossy(&self.body).into_owned())
74    }
75
76    /// Reads the body as UTF-8 after checking for a success status.
77    pub async fn text(self) -> Result<String> {
78        self.into_text()
79    }
80
81    /// Returns the body after checking for a success status.
82    pub fn into_bytes_checked(self) -> Result<Bytes> {
83        self.error_for_status()?;
84        Ok(self.body)
85    }
86
87    /// Returns the body after checking for a success status.
88    pub async fn bytes_checked(self) -> Result<Bytes> {
89        self.into_bytes_checked()
90    }
91
92    #[cfg(feature = "json")]
93    pub fn into_json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
94        self.error_for_status()?;
95        crate::json_parser::deserialize(&self.body, self.status, self.json_parser.as_ref())
96    }
97
98    #[cfg(feature = "json")]
99    pub async fn json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
100        self.into_json()
101    }
102
103    #[cfg(feature = "json")]
104    pub fn into_json_unchecked<T: serde::de::DeserializeOwned>(self) -> Result<T> {
105        crate::json_parser::deserialize(&self.body, self.status, self.json_parser.as_ref())
106    }
107
108    #[cfg(feature = "json")]
109    pub async fn json_unchecked<T: serde::de::DeserializeOwned>(self) -> Result<T> {
110        self.into_json_unchecked()
111    }
112
113    /// Deserialize JSON and run [`garde::Validate`] rules (feature `validate`).
114    #[cfg(feature = "validate")]
115    pub fn into_json_validated<T>(self) -> Result<T>
116    where
117        T: serde::de::DeserializeOwned + garde::Validate,
118        T::Context: Default,
119    {
120        self.error_for_status()?;
121        crate::validate_json::deserialize_validated(
122            &self.body,
123            self.status,
124            self.json_parser.as_ref(),
125        )
126    }
127
128    /// Deserialize JSON and run [`garde::Validate`] rules (feature `validate`).
129    #[cfg(feature = "validate")]
130    pub async fn json_validated<T>(self) -> Result<T>
131    where
132        T: serde::de::DeserializeOwned + garde::Validate,
133        T::Context: Default,
134    {
135        self.into_json_validated()
136    }
137
138    /// Like [`into_json_validated`](Self::into_json_validated) without checking HTTP status.
139    #[cfg(feature = "validate")]
140    pub fn into_json_validated_unchecked<T>(self) -> Result<T>
141    where
142        T: serde::de::DeserializeOwned + garde::Validate,
143        T::Context: Default,
144    {
145        crate::validate_json::deserialize_validated(
146            &self.body,
147            self.status,
148            self.json_parser.as_ref(),
149        )
150    }
151
152    /// Like [`into_json_validated`](Self::into_json_validated) without checking HTTP status.
153    #[cfg(feature = "validate")]
154    pub async fn json_validated_unchecked<T>(self) -> Result<T>
155    where
156        T: serde::de::DeserializeOwned + garde::Validate,
157        T::Context: Default,
158    {
159        self.into_json_validated_unchecked()
160    }
161
162    pub fn into_parts(self) -> (StatusCode, HeaderMap, Bytes) {
163        (self.status, self.headers, self.body)
164    }
165}
166
167impl std::fmt::Debug for Response {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        let mut debug = f.debug_struct("Response");
170        debug
171            .field("status", &self.status)
172            .field("headers", &self.headers)
173            .field("body", &self.body)
174            .field("url", &self.url);
175        #[cfg(feature = "json")]
176        if self.json_parser.is_some() {
177            debug.field("json_parser", &"<custom>");
178        }
179        debug.finish()
180    }
181}
182
183#[cfg(all(test, feature = "json"))]
184mod tests {
185    use super::*;
186    use serde::Deserialize;
187
188    #[derive(Debug, Deserialize, PartialEq)]
189    struct IdOnly {
190        id: u64,
191    }
192
193    #[test]
194    fn into_text_returns_body_on_success() {
195        let response = Response::new(
196            StatusCode::OK,
197            HeaderMap::new(),
198            Bytes::from_static(b"hello"),
199            None,
200            None,
201        );
202        assert_eq!(response.into_text().unwrap(), "hello");
203    }
204
205    #[test]
206    fn into_json_deserializes_without_async() {
207        let response = Response::new(
208            StatusCode::OK,
209            HeaderMap::new(),
210            Bytes::from_static(br#"{"id":7}"#),
211            None,
212            None,
213        );
214        assert_eq!(response.into_json::<IdOnly>().unwrap(), IdOnly { id: 7 });
215    }
216}