Skip to main content

http_request/response/
struct.rs

1use super::*;
2
3/// Parsed HTTP response.
4///
5/// Value type with all fields public — no `Arc<RwLock>` wrapping, no
6/// associated-type / boxed-trait indirection. Use the field accessors
7/// directly (`response.status_code`, `response.body`, ...) or the
8/// convenience helpers (`response.text()`, `response.is_success()`).
9///
10/// `HttpResponse` is `Clone` and `Debug`. Decoding is a method
11/// ([`HttpResponse::decode`]) that returns a fresh `HttpResponse` with
12/// the body expanded — original is untouched.
13#[derive(Clone, Debug, Getter, Setter)]
14pub struct HttpResponse {
15    /// HTTP version (`HTTP/1.1`, `HTTP/2`, ...).
16    pub version: HttpVersion,
17    /// 3-digit numeric status code (`200`, `404`, ...).
18    #[get(type(copy))]
19    pub status_code: ResponseStatusCode,
20    /// Reason phrase (`"OK"`, `"Not Found"`, ...).
21    #[set(type(AsRef<str>))]
22    pub reason_phrase: String,
23    /// Response headers (single-value, lowercase keys).
24    pub headers: HttpResponseHeaders,
25    /// Raw response body bytes.
26    #[set(type(AsRef<[u8]>))]
27    pub body: ResponseBody,
28}
29
30impl Default for HttpResponse {
31    fn default() -> Self {
32        Self {
33            version: HttpVersion::default(),
34            status_code: HttpStatus::Unknown.code(),
35            reason_phrase: HttpStatus::Unknown.to_string(),
36            headers: new_response_headers(),
37            body: ResponseBody::new(),
38        }
39    }
40}
41
42impl HttpResponse {
43    /// Construct from raw response bytes (status line + headers + body).
44    pub fn from_bytes(response: &[u8]) -> Self {
45        let split_lines: Vec<&[u8]> = split_multi_byte(response, HTTP_BR_BYTES);
46        let mut lines: IntoIter<&[u8]> = split_lines.into_iter();
47        let status_line: &[u8] = lines.next().unwrap_or(&[]);
48        let status_parts: Vec<&[u8]> = split_whitespace(status_line);
49        let version: HttpVersion = status_parts
50            .first()
51            .and_then(|part: &&[u8]| from_utf8(part).ok())
52            .and_then(|version_str: &str| version_str.parse::<HttpVersion>().ok())
53            .unwrap_or_default();
54        let status_code: ResponseStatusCode = status_parts
55            .get(1)
56            .and_then(|part: &&[u8]| from_utf8(part).ok())
57            .and_then(|code_str: &str| code_str.parse().ok())
58            .unwrap_or(HttpStatus::Unknown.code());
59        let reason_phrase: String = status_parts.get(2..).map_or_else(
60            || HttpStatus::Unknown.to_string(),
61            |parts: &[&[u8]]| {
62                if parts.is_empty() {
63                    HttpStatus::Unknown.to_string()
64                } else if parts.len() == 1 {
65                    String::from_utf8_lossy(parts[0]).into_owned()
66                } else {
67                    let total_len: usize =
68                        parts.iter().map(|p: &&[u8]| p.len()).sum::<usize>() + parts.len() - 1;
69                    let mut result: String = String::with_capacity(total_len);
70                    for (i, part) in parts.iter().enumerate() {
71                        if i > 0 {
72                            result.push(' ');
73                        }
74                        result.push_str(&String::from_utf8_lossy(part));
75                    }
76                    result
77                }
78            },
79        );
80        let mut headers: HttpResponseHeaders = new_response_headers();
81        for line in lines.by_ref() {
82            if line.is_empty() {
83                break;
84            }
85            let mut colon_pos: Option<usize> = None;
86            for (i, &byte) in line.iter().enumerate() {
87                if byte == COLON_U8 {
88                    colon_pos = Some(i);
89                    break;
90                }
91            }
92            if let Some(pos) = colon_pos
93                && pos > 0
94                && pos + 1 < line.len()
95            {
96                let key_bytes: &[u8] = &line[..pos];
97                let value_start: usize = if line.get(pos + 1) == Some(&SPACE_U8) {
98                    pos + 2
99                } else {
100                    pos + 1
101                };
102                let value_bytes: &[u8] = &line[value_start..];
103                if let (Ok(key_str), Ok(value_str)) = (from_utf8(key_bytes), from_utf8(value_bytes))
104                {
105                    headers.insert(
106                        key_str.trim().to_ascii_lowercase(),
107                        value_str.trim().to_owned(),
108                    );
109                }
110            }
111        }
112        let body: ResponseBody = match lines.len() {
113            0 => ResponseBody::new(),
114            1 => {
115                let line: &[u8] = lines.next().unwrap_or(&[]);
116                line.to_vec()
117            }
118            _ => {
119                let lines_slice: &[&[u8]] = lines.as_slice();
120                let total_size: usize = lines_slice
121                    .iter()
122                    .map(|line: &&[u8]| line.len())
123                    .sum::<usize>()
124                    + lines_slice.len().saturating_sub(1) * BR_BYTES.len();
125                let mut body: ResponseBody = ResponseBody::with_capacity(total_size);
126                let mut first: bool = true;
127                for line in lines {
128                    if !first {
129                        body.extend_from_slice(BR_BYTES);
130                    }
131                    body.extend_from_slice(line);
132                    first = false;
133                }
134                body
135            }
136        };
137        HttpResponse {
138            version,
139            status_code,
140            reason_phrase,
141            headers,
142            body,
143        }
144    }
145
146    /// Was the response a 2xx success?
147    pub fn is_success(&self) -> bool {
148        (200..300).contains(&self.status_code)
149    }
150
151    /// Was the response a 3xx redirect?
152    pub fn is_redirect(&self) -> bool {
153        (300..400).contains(&self.status_code)
154    }
155
156    /// Look up a single header value (case-insensitive).
157    pub fn get_header<K: AsRef<str>>(&self, key: K) -> Option<&str> {
158        let normalized = key.as_ref().to_ascii_lowercase();
159        self.headers.get(&normalized).map(String::as_str)
160    }
161
162    /// Get the response body as a UTF-8 string (lossy on invalid bytes).
163    pub fn text(&self) -> String {
164        String::from_utf8_lossy(&self.body).into_owned()
165    }
166
167    /// Get the response body as raw bytes.
168    pub fn bytes(&self) -> &[u8] {
169        &self.body
170    }
171
172    /// Decode the body using the headers' Content-Encoding (gzip / deflate /
173    /// br). Returns a fresh response with the decoded body — the original is
174    /// untouched. `buffer_size` controls the chunk size for streaming decoders.
175    pub fn decode(&self, buffer_size: usize) -> HttpResponse {
176        let flat_headers: HttpResponseHeaders = self.headers.clone();
177        let decoded: ResponseBody = Compress::from(&flat_headers)
178            .decode(&self.body, buffer_size)
179            .into_owned();
180        HttpResponse {
181            version: self.version.clone(),
182            status_code: self.status_code,
183            reason_phrase: self.reason_phrase.clone(),
184            headers: self.headers.clone(),
185            body: decoded,
186        }
187    }
188}