crux_http/response/
raw_response.rs1use http::{HeaderMap, HeaderName, HeaderValue, StatusCode, Version};
2use serde::de::DeserializeOwned;
3use std::{fmt, ops::Index};
4
5use super::decode::decode_body;
6use crate::{HttpError, Result, protocol::HttpResponse};
7
8pub struct RawResponse {
10 status: StatusCode,
11 version: Option<Version>,
12 headers: HeaderMap,
13 body: Vec<u8>,
14}
15
16impl RawResponse {
17 pub(crate) const fn new(status: StatusCode, headers: HeaderMap, body: Vec<u8>) -> Self {
19 Self {
20 status,
21 version: None,
22 headers,
23 body,
24 }
25 }
26
27 #[must_use]
39 #[allow(clippy::missing_const_for_fn)]
40 pub fn status(&self) -> StatusCode {
41 self.status
42 }
43
44 #[must_use]
57 #[allow(clippy::missing_const_for_fn)]
58 pub fn version(&self) -> Option<Version> {
59 self.version
60 }
61
62 pub fn header_all(
64 &self,
65 name: impl http::header::AsHeaderName,
66 ) -> http::header::GetAll<'_, HeaderValue> {
67 self.headers.get_all(name)
68 }
69
70 pub fn header(&self, name: impl http::header::AsHeaderName) -> Option<&HeaderValue> {
82 self.headers.get(name)
83 }
84
85 pub fn header_mut(
87 &mut self,
88 name: impl http::header::AsHeaderName,
89 ) -> Option<&mut HeaderValue> {
90 self.headers.get_mut(name)
91 }
92
93 pub fn remove_header(&mut self, name: impl http::header::AsHeaderName) -> Option<HeaderValue> {
95 self.headers.remove(name)
96 }
97
98 pub fn insert_header(
102 &mut self,
103 name: impl http::header::IntoHeaderName,
104 value: HeaderValue,
105 ) -> Option<HeaderValue> {
106 self.headers.insert(name, value)
107 }
108
109 pub fn append_header(
114 &mut self,
115 name: impl http::header::IntoHeaderName,
116 value: HeaderValue,
117 ) -> bool {
118 self.headers.append(name, value)
119 }
120
121 #[must_use]
123 pub fn iter(&self) -> http::header::Iter<'_, HeaderValue> {
124 self.headers.iter()
125 }
126
127 #[must_use]
129 pub fn iter_mut(&mut self) -> http::header::IterMut<'_, HeaderValue> {
130 self.headers.iter_mut()
131 }
132
133 #[must_use]
135 pub fn header_names(&self) -> http::header::Keys<'_, HeaderValue> {
136 self.headers.keys()
137 }
138
139 #[must_use]
141 pub fn header_values(&self) -> http::header::Values<'_, HeaderValue> {
142 self.headers.values()
143 }
144
145 #[must_use]
157 pub fn content_type(&self) -> Option<mime::Mime> {
158 self.headers
159 .get(http::header::CONTENT_TYPE)?
160 .to_str()
161 .ok()?
162 .parse()
163 .ok()
164 }
165
166 #[must_use]
168 #[allow(clippy::missing_const_for_fn)]
169 pub fn len(&self) -> Option<usize> {
170 Some(self.body.len())
171 }
172
173 #[must_use]
175 #[allow(clippy::missing_const_for_fn)]
176 pub fn is_empty(&self) -> Option<bool> {
177 Some(self.body.is_empty())
178 }
179
180 pub fn body_bytes(&mut self) -> Result<Vec<u8>> {
196 Ok(std::mem::take(&mut self.body))
197 }
198
199 pub fn body_string(&mut self) -> Result<String> {
215 let bytes = self.body_bytes()?;
216 let mime = self.content_type();
217 let claimed_encoding = mime
218 .as_ref()
219 .and_then(|m| m.get_param(mime::CHARSET))
220 .map(|name| name.as_str().to_owned());
221 Ok(decode_body(bytes, claimed_encoding.as_deref())?)
222 }
223
224 pub fn body_json<T: DeserializeOwned>(&mut self) -> Result<T> {
243 let body_bytes = self.body_bytes()?;
244 serde_json::from_slice(&body_bytes).map_err(HttpError::from)
245 }
246
247 pub fn body_form<T: DeserializeOwned>(&mut self) -> Result<T> {
266 let bytes = self.body_bytes()?;
267 serde_qs::from_bytes(&bytes).map_err(HttpError::from)
268 }
269}
270
271impl AsRef<HeaderMap> for RawResponse {
272 fn as_ref(&self) -> &HeaderMap {
273 &self.headers
274 }
275}
276
277impl AsMut<HeaderMap> for RawResponse {
278 fn as_mut(&mut self) -> &mut HeaderMap {
279 &mut self.headers
280 }
281}
282
283impl TryFrom<HttpResponse> for RawResponse {
284 type Error = HttpError;
285
286 fn try_from(r: HttpResponse) -> Result<Self> {
287 let HttpResponse {
288 status: status_u16,
289 headers: header_fields,
290 body,
291 } = r;
292
293 let status = StatusCode::from_u16(status_u16).map_err(|_| HttpError::Http {
294 code: status_u16,
295 message: format!("invalid HTTP status code: {status_u16}"),
296 body: None,
297 })?;
298
299 let mut headers = HeaderMap::new();
300 for header in header_fields {
301 if let (Ok(name), Ok(value)) = (
302 HeaderName::from_bytes(header.name.as_bytes()),
303 HeaderValue::from_str(&header.value),
304 ) {
305 headers.append(name, value);
306 }
307 }
308 Ok(Self::new(status, headers, body))
309 }
310}
311
312impl<'a> IntoIterator for &'a RawResponse {
313 type Item = (&'a HeaderName, &'a HeaderValue);
314 type IntoIter = http::header::Iter<'a, HeaderValue>;
315 fn into_iter(self) -> Self::IntoIter {
316 self.headers.iter()
317 }
318}
319
320impl<'a> IntoIterator for &'a mut RawResponse {
321 type Item = (&'a HeaderName, &'a mut HeaderValue);
322 type IntoIter = http::header::IterMut<'a, HeaderValue>;
323 fn into_iter(self) -> Self::IntoIter {
324 self.headers.iter_mut()
325 }
326}
327
328impl fmt::Debug for RawResponse {
329 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330 f.debug_struct("RawResponse")
331 .field("status", &self.status)
332 .field("headers", &self.headers)
333 .finish_non_exhaustive()
334 }
335}
336
337impl Index<&str> for RawResponse {
338 type Output = HeaderValue;
339
340 #[inline]
346 fn index(&self, name: &str) -> &HeaderValue {
347 &self.headers[name]
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use crate::protocol::HttpResponse;
355
356 #[test]
357 fn from_http_response_preserves_non_standard_status_499() {
358 let http_response = HttpResponse::status(499)
359 .body(b"client closed connection".to_vec())
360 .build();
361 let raw = RawResponse::try_from(http_response).expect("499 is a valid status code");
362 assert_eq!(raw.status().as_u16(), 499);
363 assert!(raw.status().is_client_error());
364 }
365
366 #[test]
367 fn from_http_response_preserves_non_standard_status_103() {
368 let http_response = HttpResponse::status(103).body(b"".to_vec()).build();
371 let raw = RawResponse::try_from(http_response).expect("103 is a valid status code");
372 assert_eq!(raw.status().as_u16(), 103);
373 }
374
375 #[test]
376 fn from_http_response_preserves_edge_case_status_599() {
377 let http_response = HttpResponse::status(599)
378 .body(b"custom server error".to_vec())
379 .build();
380 let raw = RawResponse::try_from(http_response).expect("599 is a valid status code");
381 assert_eq!(raw.status().as_u16(), 599);
382 assert!(raw.status().is_server_error());
383 }
384
385 #[test]
386 fn from_http_response_rejects_out_of_range_status() {
387 for bad_code in [0u16, 99, 1000, u16::MAX] {
388 let http_response = HttpResponse::status(bad_code).body(b"".to_vec()).build();
389 let err = RawResponse::try_from(http_response)
390 .expect_err(&format!("{bad_code} should be rejected"));
391 assert!(
392 matches!(err, HttpError::Http { code, .. } if code == bad_code),
393 "expected HttpError::Http with code {bad_code}, got: {err:?}"
394 );
395 }
396 }
397}