Skip to main content

cdk_http_client/
response.rs

1//! HTTP response types
2
3use core::fmt;
4
5use serde::de::DeserializeOwned;
6
7use crate::error::HttpError;
8
9/// HTTP Response type - generic over the body type R and error type E
10/// This is the primary return type for all HTTP operations
11pub type Response<R, E = HttpError> = Result<R, E>;
12
13/// Raw HTTP response with status code and body access
14pub struct RawResponse {
15    status: u16,
16    pub(crate) body: Vec<u8>,
17}
18
19impl fmt::Debug for RawResponse {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        f.debug_struct("RawResponse")
22            .field("status", &self.status)
23            .field("body_len", &self.body.len())
24            .finish()
25    }
26}
27
28impl RawResponse {
29    /// Create a new RawResponse from status and body bytes
30    pub fn new(status: u16, body: Vec<u8>) -> Self {
31        Self { status, body }
32    }
33
34    /// Get the HTTP status code
35    pub fn status(&self) -> u16 {
36        self.status
37    }
38
39    /// Check if the response status is a success (2xx)
40    pub fn is_success(&self) -> bool {
41        (200..300).contains(&self.status)
42    }
43
44    /// Check if the response status is a client error (4xx)
45    pub fn is_client_error(&self) -> bool {
46        (400..500).contains(&self.status)
47    }
48
49    /// Check if the response status is a server error (5xx)
50    pub fn is_server_error(&self) -> bool {
51        (500..600).contains(&self.status)
52    }
53
54    /// Response body as lossy UTF-8 string
55    pub fn body_lossy(&self) -> String {
56        String::from_utf8_lossy(&self.body).into_owned()
57    }
58
59    /// Deserialize JSON from a successful response, or return `HttpError::Status`
60    pub fn json_or_status_error<T: DeserializeOwned>(self) -> Response<T> {
61        if !self.is_success() {
62            return Err(HttpError::Status {
63                status: self.status,
64                message: self.body_lossy(),
65            });
66        }
67        serde_json::from_slice(&self.body).map_err(HttpError::from)
68    }
69
70    /// Get the response body as text
71    pub async fn text(self) -> Response<String> {
72        String::from_utf8(self.body).map_err(|e| HttpError::Other(e.to_string()))
73    }
74
75    /// Get the response body as JSON
76    pub async fn json<T: DeserializeOwned>(self) -> Response<T> {
77        serde_json::from_slice(&self.body).map_err(HttpError::from)
78    }
79
80    /// Get the response body as bytes
81    pub async fn bytes(self) -> Response<Vec<u8>> {
82        Ok(self.body)
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    // Note: RawResponse tests require a real response,
91    // so they are in tests/integration.rs using mockito.
92
93    #[test]
94    fn test_response_type_is_result() {
95        // Response<R, E> is just a type alias for Result<R, E>
96        let success: Response<i32> = Ok(42);
97        assert!(success.is_ok());
98        assert!(matches!(success, Ok(42)));
99
100        let error: Response<i32> = Err(HttpError::Timeout);
101        assert!(error.is_err());
102        assert!(matches!(error, Err(HttpError::Timeout)));
103    }
104
105    #[test]
106    fn raw_response_debug_redacts_body_and_reports_length() {
107        let secret = "response-body-secret";
108        let response = RawResponse::new(200, secret.as_bytes().to_vec());
109
110        let debug = format!("{response:?}");
111
112        assert!(debug.contains("status: 200"));
113        assert!(debug.contains(&format!("body_len: {}", secret.len())));
114        assert!(!debug.contains(secret));
115    }
116}