Skip to main content

crux_http/response/
raw_response.rs

1use http::{HeaderMap, HeaderName, HeaderValue, StatusCode, Version};
2use serde::de::DeserializeOwned;
3use std::{fmt, ops::Index};
4
5use super::decode::decode_body;
6use crate::{HttpError, Result, protocol::HttpResponse};
7
8/// An in-memory HTTP response as returned by the shell, used in the middleware chain.
9pub struct RawResponse {
10    status: StatusCode,
11    version: Option<Version>,
12    headers: HeaderMap,
13    body: Vec<u8>,
14}
15
16impl RawResponse {
17    /// Create a new instance directly from parts.
18    pub(crate) const fn new(status: StatusCode, headers: HeaderMap, body: Vec<u8>) -> Self {
19        Self {
20            status,
21            version: None,
22            headers,
23            body,
24        }
25    }
26
27    /// Get the HTTP status code.
28    ///
29    /// # Examples
30    ///
31    /// ```no_run
32    /// # use crux_http::client::Client;
33    /// # async fn middleware(client: Client) -> crux_http::Result<()> {
34    /// let res = client.get("https://httpbin.org/get").await?;
35    /// assert_eq!(res.status(), 200);
36    /// # Ok(()) }
37    /// ```
38    #[must_use]
39    #[allow(clippy::missing_const_for_fn)]
40    pub fn status(&self) -> StatusCode {
41        self.status
42    }
43
44    /// Get the HTTP protocol version.
45    ///
46    /// # Examples
47    ///
48    /// ```no_run
49    /// # use crux_http::client::Client;
50    /// # async fn middleware(client: Client) -> crux_http::Result<()> {
51    /// use crux_http::http::Version;
52    /// let res = client.get("https://httpbin.org/get").await?;
53    /// assert_eq!(res.version(), Some(Version::HTTP_11));
54    /// # Ok(()) }
55    /// ```
56    #[must_use]
57    #[allow(clippy::missing_const_for_fn)]
58    pub fn version(&self) -> Option<Version> {
59        self.version
60    }
61
62    /// Get all values for a header name.
63    pub fn header_all(
64        &self,
65        name: impl http::header::AsHeaderName,
66    ) -> http::header::GetAll<'_, HeaderValue> {
67        self.headers.get_all(name)
68    }
69
70    /// Get a header value by name (returns the first value for that name).
71    ///
72    /// # Examples
73    ///
74    /// ```no_run
75    /// # use crux_http::client::Client;
76    /// # async fn middleware(client: Client) -> crux_http::Result<()> {
77    /// let res = client.get("https://httpbin.org/get").await?;
78    /// assert!(res.header("Content-Length").is_some());
79    /// # Ok(()) }
80    /// ```
81    pub fn header(&self, name: impl http::header::AsHeaderName) -> Option<&HeaderValue> {
82        self.headers.get(name)
83    }
84
85    /// Get a header value mutably.
86    pub fn header_mut(
87        &mut self,
88        name: impl http::header::AsHeaderName,
89    ) -> Option<&mut HeaderValue> {
90        self.headers.get_mut(name)
91    }
92
93    /// Remove a header.
94    pub fn remove_header(&mut self, name: impl http::header::AsHeaderName) -> Option<HeaderValue> {
95        self.headers.remove(name)
96    }
97
98    /// Insert an HTTP header, replacing any existing value.
99    ///
100    /// Returns the previous value for that header name, if any.
101    pub fn insert_header(
102        &mut self,
103        name: impl http::header::IntoHeaderName,
104        value: HeaderValue,
105    ) -> Option<HeaderValue> {
106        self.headers.insert(name, value)
107    }
108
109    /// Append an HTTP header, keeping any existing values.
110    ///
111    /// Returns `true` if the value was appended to an existing entry, `false` if it was the first
112    /// value for that name.
113    pub fn append_header(
114        &mut self,
115        name: impl http::header::IntoHeaderName,
116        value: HeaderValue,
117    ) -> bool {
118        self.headers.append(name, value)
119    }
120
121    /// An iterator visiting all header (name, value) pairs in arbitrary order.
122    #[must_use]
123    pub fn iter(&self) -> http::header::Iter<'_, HeaderValue> {
124        self.headers.iter()
125    }
126
127    /// An iterator visiting all header (name, value) pairs with mutable values.
128    #[must_use]
129    pub fn iter_mut(&mut self) -> http::header::IterMut<'_, HeaderValue> {
130        self.headers.iter_mut()
131    }
132
133    /// An iterator visiting all header names in arbitrary order.
134    #[must_use]
135    pub fn header_names(&self) -> http::header::Keys<'_, HeaderValue> {
136        self.headers.keys()
137    }
138
139    /// An iterator visiting all header values in arbitrary order.
140    #[must_use]
141    pub fn header_values(&self) -> http::header::Values<'_, HeaderValue> {
142        self.headers.values()
143    }
144
145    /// Get the response content type as a `Mime`.
146    ///
147    /// # Examples
148    ///
149    /// ```no_run
150    /// # use crux_http::client::Client;
151    /// # async fn middleware(client: Client) -> crux_http::Result<()> {
152    /// let res = client.get("https://httpbin.org/json").await?;
153    /// assert_eq!(res.content_type(), Some(mime::APPLICATION_JSON));
154    /// # Ok(()) }
155    /// ```
156    #[must_use]
157    pub fn content_type(&self) -> Option<mime::Mime> {
158        self.headers
159            .get(http::header::CONTENT_TYPE)?
160            .to_str()
161            .ok()?
162            .parse()
163            .ok()
164    }
165
166    /// Get the length of the body in bytes.
167    #[must_use]
168    #[allow(clippy::missing_const_for_fn)]
169    pub fn len(&self) -> Option<usize> {
170        Some(self.body.len())
171    }
172
173    /// Returns `true` if the body is empty.
174    #[must_use]
175    #[allow(clippy::missing_const_for_fn)]
176    pub fn is_empty(&self) -> Option<bool> {
177        Some(self.body.is_empty())
178    }
179
180    /// Reads the entire response body into a byte buffer, leaving it empty.
181    ///
182    /// # Errors
183    ///
184    /// This function currently never returns an error; the `Result` wrapper is kept for API consistency.
185    ///
186    /// # Examples
187    ///
188    /// ```no_run
189    /// # use crux_http::client::Client;
190    /// # async fn middleware(client: Client) -> crux_http::Result<()> {
191    /// let mut res = client.get("https://httpbin.org/get").await?;
192    /// let bytes: Vec<u8> = res.body_bytes()?;
193    /// # Ok(()) }
194    /// ```
195    pub fn body_bytes(&mut self) -> Result<Vec<u8>> {
196        Ok(std::mem::take(&mut self.body))
197    }
198
199    /// Reads the entire response body into a string.
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if the body contains invalid UTF-8.
204    ///
205    /// # Examples
206    ///
207    /// ```no_run
208    /// # use crux_http::client::Client;
209    /// # async fn middleware(client: Client) -> crux_http::Result<()> {
210    /// let mut res = client.get("https://httpbin.org/get").await?;
211    /// let string: String = res.body_string()?;
212    /// # Ok(()) }
213    /// ```
214    pub fn body_string(&mut self) -> Result<String> {
215        let bytes = self.body_bytes()?;
216        let mime = self.content_type();
217        let claimed_encoding = mime
218            .as_ref()
219            .and_then(|m| m.get_param(mime::CHARSET))
220            .map(|name| name.as_str().to_owned());
221        Ok(decode_body(bytes, claimed_encoding.as_deref())?)
222    }
223
224    /// Reads and deserializes the entire response body from JSON.
225    ///
226    /// # Errors
227    ///
228    /// Returns an error if deserialisation fails.
229    ///
230    /// # Examples
231    ///
232    /// ```no_run
233    /// # use serde::{Deserialize, Serialize};
234    /// # use crux_http::client::Client;
235    /// # async fn middleware(client: Client) -> crux_http::Result<()> {
236    /// #[derive(Deserialize, Serialize)]
237    /// struct Ip { ip: String }
238    /// let mut res = client.get("https://api.ipify.org?format=json").await?;
239    /// let Ip { ip } = res.body_json()?;
240    /// # Ok(()) }
241    /// ```
242    pub fn body_json<T: DeserializeOwned>(&mut self) -> Result<T> {
243        let body_bytes = self.body_bytes()?;
244        serde_json::from_slice(&body_bytes).map_err(HttpError::from)
245    }
246
247    /// Reads and deserializes the entire response body from form encoding.
248    ///
249    /// # Errors
250    ///
251    /// Returns an error if deserialisation fails.
252    ///
253    /// # Examples
254    ///
255    /// ```no_run
256    /// # use serde::{Deserialize, Serialize};
257    /// # use crux_http::client::Client;
258    /// # async fn middleware(client: Client) -> crux_http::Result<()> {
259    /// #[derive(Deserialize, Serialize)]
260    /// struct Body { apples: u32 }
261    /// let mut res = client.get("https://api.example.com/v1/response").await?;
262    /// let Body { apples } = res.body_form()?;
263    /// # Ok(()) }
264    /// ```
265    pub fn body_form<T: DeserializeOwned>(&mut self) -> Result<T> {
266        let bytes = self.body_bytes()?;
267        serde_qs::from_bytes(&bytes).map_err(HttpError::from)
268    }
269}
270
271impl AsRef<HeaderMap> for RawResponse {
272    fn as_ref(&self) -> &HeaderMap {
273        &self.headers
274    }
275}
276
277impl AsMut<HeaderMap> for RawResponse {
278    fn as_mut(&mut self) -> &mut HeaderMap {
279        &mut self.headers
280    }
281}
282
283impl TryFrom<HttpResponse> for RawResponse {
284    type Error = HttpError;
285
286    fn try_from(r: HttpResponse) -> Result<Self> {
287        let HttpResponse {
288            status: status_u16,
289            headers: header_fields,
290            body,
291        } = r;
292
293        let status = StatusCode::from_u16(status_u16).map_err(|_| HttpError::Http {
294            code: status_u16,
295            message: format!("invalid HTTP status code: {status_u16}"),
296            body: None,
297        })?;
298
299        let mut headers = HeaderMap::new();
300        for header in header_fields {
301            if let (Ok(name), Ok(value)) = (
302                HeaderName::from_bytes(header.name.as_bytes()),
303                HeaderValue::from_str(&header.value),
304            ) {
305                headers.append(name, value);
306            }
307        }
308        Ok(Self::new(status, headers, body))
309    }
310}
311
312impl<'a> IntoIterator for &'a RawResponse {
313    type Item = (&'a HeaderName, &'a HeaderValue);
314    type IntoIter = http::header::Iter<'a, HeaderValue>;
315    fn into_iter(self) -> Self::IntoIter {
316        self.headers.iter()
317    }
318}
319
320impl<'a> IntoIterator for &'a mut RawResponse {
321    type Item = (&'a HeaderName, &'a mut HeaderValue);
322    type IntoIter = http::header::IterMut<'a, HeaderValue>;
323    fn into_iter(self) -> Self::IntoIter {
324        self.headers.iter_mut()
325    }
326}
327
328impl fmt::Debug for RawResponse {
329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330        f.debug_struct("RawResponse")
331            .field("status", &self.status)
332            .field("headers", &self.headers)
333            .finish_non_exhaustive()
334    }
335}
336
337impl Index<&str> for RawResponse {
338    type Output = HeaderValue;
339
340    /// Returns a reference to the first header value for the given name.
341    ///
342    /// # Panics
343    ///
344    /// Panics if the name is not present in `RawResponse`.
345    #[inline]
346    fn index(&self, name: &str) -> &HeaderValue {
347        &self.headers[name]
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354    use crate::protocol::HttpResponse;
355
356    #[test]
357    fn from_http_response_preserves_non_standard_status_499() {
358        let http_response = HttpResponse::status(499)
359            .body(b"client closed connection".to_vec())
360            .build();
361        let raw = RawResponse::try_from(http_response).expect("499 is a valid status code");
362        assert_eq!(raw.status().as_u16(), 499);
363        assert!(raw.status().is_client_error());
364    }
365
366    #[test]
367    fn from_http_response_preserves_non_standard_status_103() {
368        // 103 Early Hints (StatusCode::EARLY_HINTS) - verify informational codes are
369        // preserved and not mistakenly treated as errors.
370        let http_response = HttpResponse::status(103).body(b"".to_vec()).build();
371        let raw = RawResponse::try_from(http_response).expect("103 is a valid status code");
372        assert_eq!(raw.status().as_u16(), 103);
373    }
374
375    #[test]
376    fn from_http_response_preserves_edge_case_status_599() {
377        let http_response = HttpResponse::status(599)
378            .body(b"custom server error".to_vec())
379            .build();
380        let raw = RawResponse::try_from(http_response).expect("599 is a valid status code");
381        assert_eq!(raw.status().as_u16(), 599);
382        assert!(raw.status().is_server_error());
383    }
384
385    #[test]
386    fn from_http_response_rejects_out_of_range_status() {
387        for bad_code in [0u16, 99, 1000, u16::MAX] {
388            let http_response = HttpResponse::status(bad_code).body(b"".to_vec()).build();
389            let err = RawResponse::try_from(http_response)
390                .expect_err(&format!("{bad_code} should be rejected"));
391            assert!(
392                matches!(err, HttpError::Http { code, .. } if code == bad_code),
393                "expected HttpError::Http with code {bad_code}, got: {err:?}"
394            );
395        }
396    }
397}