Skip to main content

rainier_http_client/
response.rs

1//! What came back — [`HttpResponse`].
2
3use std::collections::BTreeMap;
4
5use rainier_support::{Error, Result};
6use serde::de::DeserializeOwned;
7
8use crate::transport::RawResponse;
9
10/// One response, with its body already read.
11#[derive(Debug, Clone)]
12pub struct HttpResponse {
13    status: u16,
14    headers: BTreeMap<String, String>,
15    body: Vec<u8>,
16}
17
18impl HttpResponse {
19    /// Wrap what a transport returned.
20    pub fn new(raw: RawResponse) -> Self {
21        Self { status: raw.status, headers: raw.headers, body: raw.body }
22    }
23
24    /// Build one directly. For a fake, and for a test.
25    pub fn with(status: u16, body: impl Into<Vec<u8>>) -> Self {
26        Self { status, headers: BTreeMap::new(), body: body.into() }
27    }
28
29    /// Add a header.
30    #[must_use = "this returns the response with the header added"]
31    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
32        self.headers.insert(name.into().to_ascii_lowercase(), value.into());
33        self
34    }
35
36    /// The status.
37    pub fn status(&self) -> u16 {
38        self.status
39    }
40
41    /// Whether the status is `2xx`.
42    pub fn is_success(&self) -> bool {
43        (200..300).contains(&self.status)
44    }
45
46    /// Whether the status is `4xx`.
47    pub fn is_client_error(&self) -> bool {
48        (400..500).contains(&self.status)
49    }
50
51    /// Whether the status is `5xx`.
52    pub fn is_server_error(&self) -> bool {
53        self.status >= 500
54    }
55
56    /// A header's value, however it was capitalised.
57    pub fn header(&self, name: &str) -> Option<&str> {
58        self.headers.get(&name.to_ascii_lowercase()).map(String::as_str)
59    }
60
61    /// Every header.
62    pub fn headers(&self) -> &BTreeMap<String, String> {
63        &self.headers
64    }
65
66    /// The body, as bytes.
67    pub fn bytes(&self) -> &[u8] {
68        &self.body
69    }
70
71    /// The body, as text.
72    pub fn text(&self) -> String {
73        String::from_utf8_lossy(&self.body).into_owned()
74    }
75
76    /// The body, deserialised.
77    ///
78    /// # Errors
79    ///
80    /// Quoting the start of the body, because a parse failure here is almost
81    /// always an error page or a rate-limit notice where JSON was expected,
82    /// and `expected value at line 1 column 1` says nothing about which.
83    pub fn json<T: DeserializeOwned>(&self) -> Result<T> {
84        serde_json::from_slice(&self.body).map_err(|e| {
85            let preview: String = self.text().chars().take(400).collect();
86            Error::internal(format!(
87                "the response body is not the expected JSON: {e}\n  status: {}\n  body: {preview}",
88                self.status
89            ))
90        })
91    }
92
93    /// Turn a `4xx` or `5xx` into an error.
94    ///
95    /// Not automatic, and deliberately: plenty of callers want to look at a
96    /// `404` rather than propagate it, and a client that made that decision
97    /// for them would have them parsing the error back apart.
98    ///
99    /// The message carries the status and the start of the body, which is
100    /// where the other end put the reason.
101    pub fn error_for_status(self) -> Result<Self> {
102        if self.is_success() {
103            return Ok(self);
104        }
105
106        let preview: String = self.text().chars().take(400).collect();
107        let message = format!("the request failed with {}: {preview}", self.status);
108
109        Err(if self.is_server_error() {
110            Error::service_unavailable(message)
111        } else {
112            Error::bad_request(message)
113        })
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use serde::Deserialize;
121
122    #[test]
123    fn a_status_reads_as_the_class_it_is_in() {
124        assert!(HttpResponse::with(200, "").is_success());
125        assert!(HttpResponse::with(204, "").is_success());
126        assert!(HttpResponse::with(404, "").is_client_error());
127        assert!(HttpResponse::with(503, "").is_server_error());
128
129        assert!(!HttpResponse::with(302, "").is_success());
130        assert!(!HttpResponse::with(500, "").is_client_error());
131    }
132
133    #[test]
134    fn headers_are_found_whatever_case_they_are_asked_for_in() {
135        let response = HttpResponse::with(200, "").with_header("Content-Type", "application/json");
136
137        assert_eq!(response.header("content-type"), Some("application/json"));
138        assert_eq!(response.header("CONTENT-TYPE"), Some("application/json"));
139    }
140
141    #[test]
142    fn a_json_body_deserialises() {
143        #[derive(Debug, Deserialize, PartialEq)]
144        struct Body {
145            id: u64,
146        }
147
148        let response = HttpResponse::with(200, r#"{"id":42}"#);
149
150        assert_eq!(response.json::<Body>().unwrap(), Body { id: 42 });
151    }
152
153    #[test]
154    fn a_body_that_is_not_json_says_what_it_was() {
155        // The failure this message exists for: an HTML error page where JSON
156        // was expected, which `expected value at line 1 column 1` describes
157        // uselessly.
158        let response = HttpResponse::with(502, "<html><body>Bad Gateway</body></html>");
159
160        let error = response.json::<serde_json::Value>().unwrap_err();
161
162        assert!(error.message().contains("502"), "{}", error.message());
163        assert!(error.message().contains("Bad Gateway"), "{}", error.message());
164    }
165
166    #[test]
167    fn error_for_status_leaves_a_success_alone() {
168        let response = HttpResponse::with(200, "fine").error_for_status().unwrap();
169
170        assert_eq!(response.text(), "fine");
171    }
172
173    #[test]
174    fn error_for_status_distinguishes_whose_fault_it_was() {
175        // A 4xx is this caller's problem to fix; a 5xx is worth retrying.
176        // Rendering both as the same kind loses that.
177        let ours = HttpResponse::with(422, "bad payload").error_for_status().unwrap_err();
178        let theirs = HttpResponse::with(503, "try later").error_for_status().unwrap_err();
179
180        assert_eq!(ours.status(), 400);
181        assert_eq!(theirs.status(), 503);
182    }
183
184    #[test]
185    fn the_error_carries_what_the_other_end_said() {
186        let error = HttpResponse::with(400, r#"{"error":"missing field: email"}"#)
187            .error_for_status()
188            .unwrap_err();
189
190        assert!(error.message().contains("missing field: email"), "{}", error.message());
191    }
192
193    #[test]
194    fn a_very_long_body_is_truncated_in_the_message() {
195        // An error message is read in a log line, and a megabyte of HTML in
196        // one is a log nobody can read.
197        let error = HttpResponse::with(500, "x".repeat(100_000)).error_for_status().unwrap_err();
198
199        assert!(error.message().len() < 600, "{} characters", error.message().len());
200    }
201}