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)
294            .map_err(|_| HttpError::InvalidStatusCode(status_u16))?;
295
296        let mut headers = HeaderMap::new();
297        for header in header_fields {
298            if let (Ok(name), Ok(value)) = (
299                HeaderName::from_bytes(header.name.as_bytes()),
300                HeaderValue::from_str(&header.value),
301            ) {
302                headers.append(name, value);
303            }
304        }
305        Ok(Self::new(status, headers, body))
306    }
307}
308
309impl<'a> IntoIterator for &'a RawResponse {
310    type Item = (&'a HeaderName, &'a HeaderValue);
311    type IntoIter = http::header::Iter<'a, HeaderValue>;
312    fn into_iter(self) -> Self::IntoIter {
313        self.headers.iter()
314    }
315}
316
317impl<'a> IntoIterator for &'a mut RawResponse {
318    type Item = (&'a HeaderName, &'a mut HeaderValue);
319    type IntoIter = http::header::IterMut<'a, HeaderValue>;
320    fn into_iter(self) -> Self::IntoIter {
321        self.headers.iter_mut()
322    }
323}
324
325impl fmt::Debug for RawResponse {
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        f.debug_struct("RawResponse")
328            .field("status", &self.status)
329            .field("headers", &self.headers)
330            .finish_non_exhaustive()
331    }
332}
333
334impl Index<&str> for RawResponse {
335    type Output = HeaderValue;
336
337    /// Returns a reference to the first header value for the given name.
338    ///
339    /// # Panics
340    ///
341    /// Panics if the name is not present in `RawResponse`.
342    #[inline]
343    fn index(&self, name: &str) -> &HeaderValue {
344        &self.headers[name]
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351    use crate::protocol::HttpResponse;
352
353    #[test]
354    fn from_http_response_preserves_non_standard_status_499() {
355        let http_response = HttpResponse::status(499)
356            .body(b"client closed connection".to_vec())
357            .build();
358        let raw = RawResponse::try_from(http_response).expect("499 is a valid status code");
359        assert_eq!(raw.status().as_u16(), 499);
360        assert!(raw.status().is_client_error());
361    }
362
363    #[test]
364    fn from_http_response_preserves_non_standard_status_103() {
365        // 103 Early Hints (StatusCode::EARLY_HINTS) - verify informational codes are
366        // preserved and not mistakenly treated as errors.
367        let http_response = HttpResponse::status(103).body(b"".to_vec()).build();
368        let raw = RawResponse::try_from(http_response).expect("103 is a valid status code");
369        assert_eq!(raw.status().as_u16(), 103);
370    }
371
372    #[test]
373    fn from_http_response_preserves_edge_case_status_599() {
374        let http_response = HttpResponse::status(599)
375            .body(b"custom server error".to_vec())
376            .build();
377        let raw = RawResponse::try_from(http_response).expect("599 is a valid status code");
378        assert_eq!(raw.status().as_u16(), 599);
379        assert!(raw.status().is_server_error());
380    }
381
382    #[test]
383    fn from_http_response_rejects_out_of_range_status() {
384        for bad_code in [0u16, 99, 1000, u16::MAX] {
385            let http_response = HttpResponse::status(bad_code).body(b"".to_vec()).build();
386            let err = RawResponse::try_from(http_response)
387                .expect_err(&format!("{bad_code} should be rejected"));
388            assert!(
389                matches!(err, HttpError::InvalidStatusCode(code) if code == bad_code),
390                "expected HttpError::InvalidStatusCode({bad_code}), got: {err:?}"
391            );
392        }
393    }
394}