Skip to main content

oxihttp_core/
response_ext.rs

1//! Extension trait providing ergonomic body-consuming methods on HTTP responses.
2//!
3//! Mirrors the reqwest `Response` API for any `http::Response<B>` where
4//! `B: http_body::Body`. All methods are async and consume the response.
5
6use bytes::{Buf, Bytes};
7use http::Response;
8use http_body_util::BodyExt as _;
9
10use crate::error::OxiHttpError;
11
12/// Extension methods for `http::Response<B>` where `B` implements
13/// [`http_body::Body`].
14///
15/// Provided for any response type where the body can be buffered
16/// asynchronously. The trait methods consume `self` (the response).
17///
18/// # Example
19///
20/// ```rust,ignore
21/// use oxihttp_core::ResponseExt;
22///
23/// let text = response.body_text().await?;
24/// let value: MyStruct = response.body_json().await?;
25/// ```
26#[allow(async_fn_in_trait)]
27pub trait ResponseExt: Sized {
28    /// Consume the response and collect all body bytes into a single [`Bytes`].
29    async fn body_bytes(self) -> Result<Bytes, OxiHttpError>;
30
31    /// Consume the response and decode the body as a UTF-8 string.
32    async fn body_text(self) -> Result<String, OxiHttpError>;
33
34    /// Consume the response and deserialize the body as JSON.
35    async fn body_json<T: serde::de::DeserializeOwned>(self) -> Result<T, OxiHttpError>;
36}
37
38impl<B> ResponseExt for Response<B>
39where
40    B: http_body::Body + Send,
41    B::Data: Buf,
42    B::Error: std::fmt::Display,
43{
44    async fn body_bytes(self) -> Result<Bytes, OxiHttpError> {
45        let body = self.into_body();
46        let collected = body
47            .collect()
48            .await
49            .map_err(|e| OxiHttpError::Body(e.to_string()))?;
50        Ok(collected.to_bytes())
51    }
52
53    async fn body_text(self) -> Result<String, OxiHttpError> {
54        let bytes = self.body_bytes().await?;
55        String::from_utf8(bytes.to_vec())
56            .map_err(|e| OxiHttpError::Body(format!("invalid UTF-8: {e}")))
57    }
58
59    async fn body_json<T: serde::de::DeserializeOwned>(self) -> Result<T, OxiHttpError> {
60        let bytes = self.body_bytes().await?;
61        serde_json::from_slice(&bytes).map_err(|e| OxiHttpError::Json(e.to_string()))
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use bytes::Bytes;
69    use http::Response;
70    use http_body_util::Full;
71
72    #[tokio::test]
73    async fn test_body_bytes() {
74        let resp: Response<Full<Bytes>> = Response::new(Full::new(Bytes::from("hello")));
75        let bytes = resp.body_bytes().await.expect("collect succeeds");
76        assert_eq!(bytes.as_ref(), b"hello");
77    }
78
79    #[tokio::test]
80    async fn test_body_text() {
81        let resp: Response<Full<Bytes>> = Response::new(Full::new(Bytes::from("hello text")));
82        let text = resp.body_text().await.expect("decode succeeds");
83        assert_eq!(text, "hello text");
84    }
85
86    #[tokio::test]
87    async fn test_body_json() {
88        #[derive(serde::Deserialize, PartialEq, Debug)]
89        struct Msg {
90            value: u32,
91        }
92
93        let json = br#"{"value":42}"#;
94        let resp: Response<Full<Bytes>> = Response::new(Full::new(Bytes::from(json.as_ref())));
95        let msg: Msg = resp.body_json().await.expect("deserialise succeeds");
96        assert_eq!(msg, Msg { value: 42 });
97    }
98
99    #[tokio::test]
100    async fn test_body_text_invalid_utf8() {
101        let resp: Response<Full<Bytes>> = Response::new(Full::new(Bytes::from(vec![0xFF, 0xFE])));
102        let result = resp.body_text().await;
103        assert!(result.is_err());
104        let err_msg = result.unwrap_err().to_string();
105        assert!(err_msg.contains("invalid UTF-8"), "got: {err_msg}");
106    }
107
108    #[tokio::test]
109    async fn test_body_json_invalid() {
110        let resp: Response<Full<Bytes>> = Response::new(Full::new(Bytes::from("not json")));
111        let result = resp.body_json::<serde_json::Value>().await;
112        assert!(result.is_err());
113    }
114}