Skip to main content

crux_http/response/
response.rs

1use super::decode::decode_body;
2use crate::{HttpError, Result};
3use http::{HeaderMap, HeaderName, HeaderValue, StatusCode, Version};
4use serde::de::DeserializeOwned;
5use std::{fmt, ops::Index};
6
7/// An HTTP Response that will be passed to an app's update function.
8#[derive(Clone, serde::Serialize, serde::Deserialize)]
9pub struct Response<Body> {
10    #[serde(skip, default)]
11    version: Option<Version>,
12    #[serde(with = "status_serde")]
13    status: StatusCode,
14    #[serde(with = "header_serde")]
15    headers: HeaderMap,
16    body: Option<Body>,
17}
18
19impl<Body> Response<Body> {
20    /// Create a new instance.
21    pub(crate) fn new(mut res: super::RawResponse) -> Result<Response<Vec<u8>>> {
22        let body = res.body_bytes()?;
23        let status = res.status();
24
25        if status.is_client_error() || status.is_server_error() {
26            return Err(HttpError::Http {
27                code: status.as_u16(),
28                message: status.to_string(),
29                body: Some(body),
30            });
31        }
32
33        let headers = res.as_ref().clone();
34
35        Ok(Response {
36            status,
37            headers,
38            version: res.version(),
39            body: Some(body),
40        })
41    }
42
43    /// Get the HTTP status code.
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// # let res = crux_http::testing::ResponseBuilder::ok().build();
49    /// assert_eq!(res.status(), 200);
50    /// ```
51    #[allow(clippy::missing_const_for_fn)]
52    pub fn status(&self) -> StatusCode {
53        self.status
54    }
55
56    /// Get the HTTP protocol version.
57    ///
58    /// # Examples
59    ///
60    /// ```no_run
61    /// # let res = crux_http::testing::ResponseBuilder::ok().build();
62    /// use crux_http::http::Version;
63    /// assert_eq!(res.version(), Some(Version::HTTP_11));
64    /// ```
65    #[allow(clippy::missing_const_for_fn)]
66    pub fn version(&self) -> Option<Version> {
67        self.version
68    }
69
70    /// Get all values for a header name.
71    pub fn header_all(
72        &self,
73        name: impl http::header::AsHeaderName,
74    ) -> http::header::GetAll<'_, HeaderValue> {
75        self.headers.get_all(name)
76    }
77
78    /// Get a header value.
79    ///
80    /// # Examples
81    ///
82    /// ```no_run
83    /// # let res = crux_http::testing::ResponseBuilder::ok()
84    /// #   .header("Content-Length", "1")
85    /// #   .build();
86    /// assert!(res.header("Content-Length").is_some());
87    /// ```
88    pub fn header(&self, name: impl http::header::AsHeaderName) -> Option<&HeaderValue> {
89        self.headers.get(name)
90    }
91
92    /// Get an HTTP header mutably.
93    pub fn header_mut(
94        &mut self,
95        name: impl http::header::AsHeaderName,
96    ) -> Option<&mut HeaderValue> {
97        self.headers.get_mut(name)
98    }
99
100    /// Remove a header.
101    pub fn remove_header(&mut self, name: impl http::header::AsHeaderName) -> Option<HeaderValue> {
102        self.headers.remove(name)
103    }
104
105    /// Insert an HTTP header, replacing any existing value.
106    ///
107    /// Returns the previous value for that header name, if any.
108    pub fn insert_header(
109        &mut self,
110        name: impl http::header::IntoHeaderName,
111        value: HeaderValue,
112    ) -> Option<HeaderValue> {
113        self.headers.insert(name, value)
114    }
115
116    /// Append an HTTP header, keeping any existing values.
117    ///
118    /// Returns `true` if the value was appended to an existing entry, `false` if it was the first
119    /// value for that name.
120    pub fn append_header(
121        &mut self,
122        name: impl http::header::IntoHeaderName,
123        value: HeaderValue,
124    ) -> bool {
125        self.headers.append(name, value)
126    }
127
128    /// An iterator visiting all header (name, value) pairs in arbitrary order.
129    #[must_use]
130    pub fn iter(&self) -> http::header::Iter<'_, HeaderValue> {
131        self.headers.iter()
132    }
133
134    /// An iterator visiting all header (name, value) pairs with mutable values.
135    #[must_use]
136    pub fn iter_mut(&mut self) -> http::header::IterMut<'_, HeaderValue> {
137        self.headers.iter_mut()
138    }
139
140    /// An iterator visiting all header names in arbitrary order.
141    #[must_use]
142    pub fn header_names(&self) -> http::header::Keys<'_, HeaderValue> {
143        self.headers.keys()
144    }
145
146    /// An iterator visiting all header values in arbitrary order.
147    #[must_use]
148    pub fn header_values(&self) -> http::header::Values<'_, HeaderValue> {
149        self.headers.values()
150    }
151
152    /// Get the response content type as a `Mime`.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// # let res = crux_http::testing::ResponseBuilder::ok()
158    /// #   .header("Content-Type", "application/json")
159    /// #   .build();
160    /// assert_eq!(res.content_type(), Some(mime::APPLICATION_JSON));
161    /// ```
162    pub fn content_type(&self) -> Option<mime::Mime> {
163        self.headers
164            .get(http::header::CONTENT_TYPE)?
165            .to_str()
166            .ok()?
167            .parse()
168            .ok()
169    }
170
171    #[allow(clippy::missing_const_for_fn)]
172    pub fn body(&self) -> Option<&Body> {
173        self.body.as_ref()
174    }
175
176    #[allow(clippy::missing_const_for_fn)]
177    pub fn take_body(&mut self) -> Option<Body> {
178        self.body.take()
179    }
180
181    pub fn with_body<NewBody>(self, body: NewBody) -> Response<NewBody> {
182        Response {
183            body: Some(body),
184            headers: self.headers,
185            status: self.status,
186            version: self.version,
187        }
188    }
189}
190
191impl<'a, Body> IntoIterator for &'a Response<Body> {
192    type Item = (&'a HeaderName, &'a HeaderValue);
193    type IntoIter = http::header::Iter<'a, HeaderValue>;
194    fn into_iter(self) -> Self::IntoIter {
195        self.iter()
196    }
197}
198
199impl<'a, Body> IntoIterator for &'a mut Response<Body> {
200    type Item = (&'a HeaderName, &'a mut HeaderValue);
201    type IntoIter = http::header::IterMut<'a, HeaderValue>;
202    fn into_iter(self) -> Self::IntoIter {
203        self.iter_mut()
204    }
205}
206
207impl Response<Vec<u8>> {
208    pub(crate) fn new_with_status(status: StatusCode) -> Self {
209        Self {
210            status,
211            headers: HeaderMap::new(),
212            version: None,
213            body: None,
214        }
215    }
216
217    /// Reads the entire request body into a byte buffer.
218    ///
219    /// # Errors
220    ///
221    /// Returns an error if the body has already been taken.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// # fn main() -> crux_http::Result<()> {
227    /// # let mut res = crux_http::testing::ResponseBuilder::ok()
228    /// #   .header("Content-Type", "application/json")
229    /// #   .body(vec![0u8, 1])
230    /// #   .build();
231    /// let bytes: Vec<u8> = res.body_bytes()?;
232    /// # Ok(()) }
233    /// ```
234    pub fn body_bytes(&mut self) -> Result<Vec<u8>> {
235        self.body.take().ok_or_else(|| HttpError::Http {
236            code: self.status().as_u16(),
237            message: "Body had no bytes".to_string(),
238            body: None,
239        })
240    }
241
242    /// Reads the entire response body into a string.
243    ///
244    /// # Errors
245    ///
246    /// Returns an error if the body has already been taken or if it contains invalid UTF-8.
247    ///
248    /// # Examples
249    ///
250    /// ```
251    /// # fn main() -> crux_http::Result<()> {
252    /// # let mut res = crux_http::testing::ResponseBuilder::ok()
253    /// #   .header("Content-Type", "application/json")
254    /// #   .body("hello".to_string().into_bytes())
255    /// #   .build();
256    /// let string: String = res.body_string()?;
257    /// assert_eq!(string, "hello");
258    /// # Ok(()) }
259    /// ```
260    pub fn body_string(&mut self) -> Result<String> {
261        let bytes = self.body_bytes()?;
262        let mime = self.content_type();
263        let claimed_encoding = mime
264            .as_ref()
265            .and_then(|m| m.get_param(mime::CHARSET))
266            .map(|name| name.as_str().to_owned());
267        Ok(decode_body(bytes, claimed_encoding.as_deref())?)
268    }
269
270    /// Reads and deserializes the entire response body from JSON.
271    ///
272    /// # Errors
273    ///
274    /// Returns an error if the body has already been taken or if deserialisation fails.
275    ///
276    /// # Examples
277    ///
278    /// ```
279    /// # use serde::{Deserialize, Serialize};
280    /// # fn main() -> crux_http::Result<()> {
281    /// # let mut res = crux_http::testing::ResponseBuilder::ok()
282    /// #   .header("Content-Type", "application/json")
283    /// #   .body("{\"ip\": \"127.0.0.1\"}".to_string().into_bytes())
284    /// #   .build();
285    /// #[derive(Deserialize, Serialize)]
286    /// struct Ip { ip: String }
287    /// let Ip { ip } = res.body_json()?;
288    /// assert_eq!(ip, "127.0.0.1");
289    /// # Ok(()) }
290    /// ```
291    pub fn body_json<T: DeserializeOwned>(&mut self) -> Result<T> {
292        let body_bytes = self.body_bytes()?;
293        serde_json::from_slice(&body_bytes).map_err(HttpError::from)
294    }
295}
296
297impl<Body> AsRef<HeaderMap> for Response<Body> {
298    fn as_ref(&self) -> &HeaderMap {
299        &self.headers
300    }
301}
302
303impl<Body> AsMut<HeaderMap> for Response<Body> {
304    fn as_mut(&mut self) -> &mut HeaderMap {
305        &mut self.headers
306    }
307}
308
309impl<Body> fmt::Debug for Response<Body> {
310    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311        f.debug_struct("Response")
312            .field("version", &self.version)
313            .field("status", &self.status)
314            .field("headers", &self.headers)
315            .finish_non_exhaustive()
316    }
317}
318
319impl<Body> Index<&str> for Response<Body> {
320    type Output = HeaderValue;
321
322    /// Returns a reference to the value corresponding to the supplied name.
323    ///
324    /// # Panics
325    ///
326    /// Panics if the name is not present in `Response`.
327    #[inline]
328    fn index(&self, name: &str) -> &HeaderValue {
329        &self.headers[name]
330    }
331}
332
333impl<Body> PartialEq for Response<Body>
334where
335    Body: PartialEq,
336{
337    fn eq(&self, other: &Self) -> bool {
338        self.status == other.status && self.headers == other.headers && self.body == other.body
339    }
340}
341
342impl<Body> Eq for Response<Body> where Body: Eq {}
343
344impl<Body> TryFrom<Response<Body>> for http::Response<Body> {
345    type Error = ();
346
347    fn try_from(res: Response<Body>) -> std::result::Result<Self, ()> {
348        let body = res.body.ok_or(())?;
349        let mut builder = http::Response::builder().status(res.status);
350        if let Some(v) = res.version {
351            builder = builder.version(v);
352        }
353        for (name, value) in &res.headers {
354            builder = builder.header(name, value);
355        }
356        builder.body(body).map_err(|_| ())
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use http::{HeaderMap, StatusCode};
363
364    use crate::{
365        HttpError, HttpResponse, RawResponse, response::Response, testing::ResponseBuilder,
366    };
367
368    #[test]
369    fn status_is_http_status_code() {
370        let res = ResponseBuilder::ok().build();
371        assert_eq!(res.status(), StatusCode::OK);
372        assert_eq!(res.status().as_u16(), 200);
373    }
374
375    #[test]
376    fn headers_are_http_header_map() {
377        let res = ResponseBuilder::ok().header("x-custom", "hello").build();
378        let map: &HeaderMap = res.as_ref();
379        assert_eq!(map["x-custom"], "hello");
380    }
381
382    #[test]
383    fn header_all_returns_multiple_values() {
384        let res = ResponseBuilder::ok()
385            .header("accept", "text/html")
386            .append_header("accept", "application/json")
387            .build();
388        let values: Vec<&str> = res
389            .header_all("accept")
390            .iter()
391            .map(|v| v.to_str().unwrap())
392            .collect();
393        assert_eq!(values, ["text/html", "application/json"]);
394    }
395
396    #[test]
397    fn native_try_from_into_http_response() {
398        use std::convert::TryFrom;
399        let res: Response<Vec<u8>> = ResponseBuilder::ok()
400            .header("x-foo", "bar")
401            .body(b"hello".to_vec())
402            .build();
403        let http_res = http::Response::<Vec<u8>>::try_from(res).unwrap();
404        assert_eq!(http_res.status(), StatusCode::OK);
405        assert_eq!(http_res.headers()["x-foo"], "bar");
406        assert_eq!(http_res.body(), b"hello");
407    }
408
409    /// Round-trip: `HttpResponse` → `crux_http::Response<Vec<u8>>` → `http::Response<Vec<u8>>`
410    #[futures_test::test]
411    async fn http_response_round_trip() {
412        use crate::protocol::HttpResponse;
413        use std::convert::TryFrom;
414
415        let http_response = HttpResponse::ok()
416            .header("content-type", "application/json")
417            .json(serde_json::json!({"data": 42}))
418            .build();
419
420        // Step 1: HttpResponse → RawResponse (via TryFrom impl in raw_response.rs)
421        let response_async = RawResponse::try_from(http_response).expect("valid status");
422
423        // Step 2: RawResponse → Response<Vec<u8>> (the path the command executor takes)
424        let response = Response::<Vec<u8>>::new(response_async).expect("should decode");
425
426        assert_eq!(response.status().as_u16(), 200);
427        assert_eq!(response.content_type(), Some(mime::APPLICATION_JSON));
428
429        // Step 3: Response<Vec<u8>> → http::Response<Vec<u8>> (native lossless conversion)
430        let http_resp = http::Response::<Vec<u8>>::try_from(response).unwrap();
431        assert_eq!(http_resp.status(), 200);
432        assert_eq!(http_resp.headers()["content-type"], "application/json");
433        let parsed: serde_json::Value = serde_json::from_slice(http_resp.body()).unwrap();
434        assert_eq!(parsed["data"], 42);
435    }
436
437    #[test]
438    fn response_status_serde_roundtrip() {
439        let res: Response<Vec<u8>> = ResponseBuilder::ok().body(vec![42u8]).build();
440        let json = serde_json::to_string(&res).expect("should serialize");
441        let back: Response<Vec<u8>> = serde_json::from_str(&json).expect("should deserialize");
442        assert_eq!(back.status().as_u16(), 200);
443        assert_eq!(back.body().unwrap(), &[42u8]);
444    }
445
446    #[test]
447    fn non_standard_status_499_becomes_http_error() {
448        // 499 is a non-standard client error (client closed connection).
449        // It arrives as HttpResponse from the shell, is converted to RawResponse,
450        // and Response::new() converts it to HttpError::Http with the original code.
451        let http_response = HttpResponse::status(499)
452            .body(b"client closed connection".to_vec())
453            .build();
454        let raw = RawResponse::try_from(http_response).expect("499 is a valid status code");
455        let result = Response::<Vec<u8>>::new(raw);
456
457        assert!(result.is_err());
458        let err = result.unwrap_err();
459        assert!(matches!(err, HttpError::Http { code, .. } if code == 499));
460    }
461
462    #[test]
463    fn non_standard_status_599_becomes_http_error() {
464        let http_response = HttpResponse::status(599)
465            .body(b"custom server error".to_vec())
466            .build();
467        let raw = RawResponse::try_from(http_response).expect("599 is a valid status code");
468        let result = Response::<Vec<u8>>::new(raw);
469
470        assert!(result.is_err());
471        let err = result.unwrap_err();
472        assert!(matches!(err, HttpError::Http { code, .. } if code == 599));
473    }
474
475    #[test]
476    fn non_standard_4xx_status_preserves_code_in_error() {
477        for status in [490, 491, 492, 493, 494, 495, 496, 497, 498, 499] {
478            let http_response = HttpResponse::status(status).body(b"".to_vec()).build();
479            let raw = RawResponse::try_from(http_response)
480                .unwrap_or_else(|_| panic!("{status} is a valid status code"));
481            let result = Response::<Vec<u8>>::new(raw);
482
483            let err = result.expect_err("should be an error");
484            assert!(
485                matches!(err, HttpError::Http { code, .. } if code == status),
486                "Expected status {status} to be preserved in HttpError, got: {err:?}"
487            );
488        }
489    }
490
491    #[test]
492    fn response_builder_with_non_standard_status_499() {
493        let res: Response<Vec<u8>> = ResponseBuilder::with_status(499).build();
494        assert_eq!(res.status().as_u16(), 499);
495    }
496
497    #[test]
498    fn response_serde_roundtrip_with_non_standard_status() {
499        let res: Response<Vec<u8>> = ResponseBuilder::with_status(499)
500            .body(b"test".to_vec())
501            .build();
502        let json = serde_json::to_string(&res).expect("should serialize");
503        let back: Response<Vec<u8>> = serde_json::from_str(&json).expect("should deserialize");
504        assert_eq!(back.status().as_u16(), 499);
505    }
506
507    #[test]
508    fn body_bytes_returns_error_when_body_already_taken() {
509        let mut res: Response<Vec<u8>> = ResponseBuilder::ok().body(b"hello".to_vec()).build();
510        let _ = res.body_bytes().unwrap();
511        let err = res.body_bytes().expect_err("second call must fail");
512        assert!(matches!(err, HttpError::Http { .. }));
513    }
514
515    #[test]
516    fn try_from_response_with_no_body_returns_err() {
517        // `new_with_status` produces body: None (used for e.g. HEAD responses).
518        let res = Response::<Vec<u8>>::new_with_status(StatusCode::OK);
519        let result = http::Response::<Vec<u8>>::try_from(res);
520        assert!(result.is_err(), "TryFrom must return Err when body is None");
521    }
522
523    #[test]
524    fn multi_value_headers_survive_serde_roundtrip() {
525        let res: Response<Vec<u8>> = ResponseBuilder::ok()
526            .header("set-cookie", "a=1")
527            .append_header("set-cookie", "b=2")
528            .body(b"".to_vec())
529            .build();
530
531        let json = serde_json::to_string(&res).expect("should serialize");
532        let back: Response<Vec<u8>> = serde_json::from_str(&json).expect("should deserialize");
533
534        let values: Vec<&str> = back
535            .header_all("set-cookie")
536            .iter()
537            .map(|v| v.to_str().unwrap())
538            .collect();
539        assert_eq!(
540            values.len(),
541            2,
542            "both Set-Cookie values must survive serde: {values:?}"
543        );
544        assert!(values.contains(&"a=1"));
545        assert!(values.contains(&"b=2"));
546    }
547}
548
549/// Custom serde for `http::StatusCode` (serialized as `u16`).
550mod status_serde {
551    use http::StatusCode;
552    use serde::{Deserialize, Deserializer, Serializer};
553
554    #[allow(clippy::trivially_copy_pass_by_ref)]
555    pub fn serialize<S: Serializer>(status: &StatusCode, ser: S) -> Result<S::Ok, S::Error> {
556        ser.serialize_u16(status.as_u16())
557    }
558
559    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<StatusCode, D::Error> {
560        let n = u16::deserialize(de)?;
561        StatusCode::from_u16(n).map_err(serde::de::Error::custom)
562    }
563}
564
565mod header_serde {
566    use http::{HeaderMap, HeaderName, HeaderValue};
567    use serde::{Deserializer, Serializer, de::Error};
568    use std::str::FromStr;
569
570    pub fn serialize<S>(headers: &HeaderMap, serializer: S) -> Result<S::Ok, S::Error>
571    where
572        S: Serializer,
573    {
574        // Group values by name, preserving insertion order via collect_map.
575        // Headers with multiple values each appear as separate entries.
576        // We build a BTreeMap so the output is deterministic.
577        let mut map: std::collections::BTreeMap<&str, Vec<&str>> =
578            std::collections::BTreeMap::new();
579        for (name, value) in headers {
580            map.entry(name.as_str())
581                .or_default()
582                .push(value.to_str().unwrap_or(""));
583        }
584        serializer.collect_map(map.iter())
585    }
586
587    pub fn deserialize<'de, D>(deserializer: D) -> Result<HeaderMap, D::Error>
588    where
589        D: Deserializer<'de>,
590    {
591        // The serialiser emits a JSON object (map); use HashMap to match.
592        let strs =
593            <std::collections::HashMap<String, Vec<String>> as serde::Deserialize>::deserialize(
594                deserializer,
595            )?;
596        let mut headers = HeaderMap::new();
597        for (name, values) in strs {
598            let name = HeaderName::from_str(&name).map_err(D::Error::custom)?;
599            for value in values {
600                let value = HeaderValue::from_str(&value).map_err(D::Error::custom)?;
601                headers.append(name.clone(), value);
602            }
603        }
604        Ok(headers)
605    }
606}