rainier_http_client/
response.rs1use std::collections::BTreeMap;
4
5use rainier_support::{Error, Result};
6use serde::de::DeserializeOwned;
7
8use crate::transport::RawResponse;
9
10#[derive(Debug, Clone)]
12pub struct HttpResponse {
13 status: u16,
14 headers: BTreeMap<String, String>,
15 body: Vec<u8>,
16}
17
18impl HttpResponse {
19 pub fn new(raw: RawResponse) -> Self {
21 Self { status: raw.status, headers: raw.headers, body: raw.body }
22 }
23
24 pub fn with(status: u16, body: impl Into<Vec<u8>>) -> Self {
26 Self { status, headers: BTreeMap::new(), body: body.into() }
27 }
28
29 #[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 pub fn status(&self) -> u16 {
38 self.status
39 }
40
41 pub fn is_success(&self) -> bool {
43 (200..300).contains(&self.status)
44 }
45
46 pub fn is_client_error(&self) -> bool {
48 (400..500).contains(&self.status)
49 }
50
51 pub fn is_server_error(&self) -> bool {
53 self.status >= 500
54 }
55
56 pub fn header(&self, name: &str) -> Option<&str> {
58 self.headers.get(&name.to_ascii_lowercase()).map(String::as_str)
59 }
60
61 pub fn headers(&self) -> &BTreeMap<String, String> {
63 &self.headers
64 }
65
66 pub fn bytes(&self) -> &[u8] {
68 &self.body
69 }
70
71 pub fn text(&self) -> String {
73 String::from_utf8_lossy(&self.body).into_owned()
74 }
75
76 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 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 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 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 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}