Skip to main content

moirai_http/server/
response.rs

1//! Validated HTTP response framing for the one-shot server.
2
3use std::io;
4
5/// A validated HTTP response owned by the application.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct HttpResponse {
8    pub(super) status: u16,
9    pub(super) headers: Vec<(String, String)>,
10    pub(super) body: Vec<u8>,
11}
12
13impl HttpResponse {
14    /// Construct a response with a status and body.
15    ///
16    /// # Errors
17    /// Returns [`io::ErrorKind::InvalidInput`] when `status` is not a final
18    /// three-digit HTTP status code.
19    pub fn new<B: Into<Vec<u8>>>(status: u16, body: B) -> io::Result<Self> {
20        if !(200..=599).contains(&status) {
21            return Err(invalid_input("HTTP response status must be three digits"));
22        }
23        Ok(Self {
24            status,
25            headers: Vec::new(),
26            body: body.into(),
27        })
28    }
29
30    /// Add one application response header.
31    ///
32    /// `Content-Length`, `Connection`, and `Transfer-Encoding` are transport
33    /// owned and cannot be overridden.
34    ///
35    /// # Errors
36    /// Returns [`io::ErrorKind::InvalidInput`] for an invalid, reserved, or
37    /// duplicate header.
38    pub fn set_header<N: Into<String>, V: Into<String>>(
39        &mut self,
40        name: N,
41        value: V,
42    ) -> io::Result<()> {
43        let name = name.into();
44        let value = value.into();
45        validate_header_name(&name)?;
46        validate_header_value(&value)?;
47        if matches!(
48            name.to_ascii_lowercase().as_str(),
49            "content-length" | "connection" | "transfer-encoding"
50        ) {
51            return Err(invalid_input(
52                "HTTP response framing header is transport-owned",
53            ));
54        }
55        if self
56            .headers
57            .iter()
58            .any(|(existing, _)| existing.eq_ignore_ascii_case(&name))
59        {
60            return Err(invalid_input("duplicate HTTP response header"));
61        }
62        self.headers.push((name, value));
63        Ok(())
64    }
65
66    /// Return the response status code.
67    #[must_use]
68    pub const fn status(&self) -> u16 {
69        self.status
70    }
71
72    /// Return response headers in insertion order.
73    #[must_use]
74    pub fn headers(&self) -> &[(String, String)] {
75        &self.headers
76    }
77
78    /// Return the response body.
79    #[must_use]
80    pub fn body(&self) -> &[u8] {
81        &self.body
82    }
83}
84
85pub(super) fn encode_response_head(
86    response: &HttpResponse,
87    max_response_bytes: usize,
88) -> io::Result<Vec<u8>> {
89    let status = response.status;
90    let reason = reason_phrase(status);
91    let mut head = Vec::new();
92    let status_line = format!("HTTP/1.1 {status} {reason}\r\n");
93    head.try_reserve(status_line.len())
94        .map_err(|_| invalid_data("HTTP response header allocation exceeds available memory"))?;
95    head.extend_from_slice(status_line.as_bytes());
96    for (name, value) in &response.headers {
97        let line_length = name
98            .len()
99            .checked_add(2)
100            .and_then(|length| length.checked_add(value.len()))
101            .and_then(|length| length.checked_add(2))
102            .ok_or_else(|| invalid_data("HTTP response header size overflows its bound"))?;
103        let projected = head
104            .len()
105            .checked_add(line_length)
106            .ok_or_else(|| invalid_data("HTTP response header size overflows its bound"))?;
107        if projected > max_response_bytes {
108            return Err(invalid_data("HTTP response exceeds configured byte bound"));
109        }
110        head.extend_from_slice(name.as_bytes());
111        head.extend_from_slice(b": ");
112        head.extend_from_slice(value.as_bytes());
113        head.extend_from_slice(b"\r\n");
114    }
115    let framing = format!(
116        "Content-Length: {}\r\nConnection: close\r\n\r\n",
117        response.body.len()
118    );
119    let projected = head
120        .len()
121        .checked_add(framing.len())
122        .ok_or_else(|| invalid_data("HTTP response header size overflows its bound"))?;
123    if projected > max_response_bytes {
124        return Err(invalid_data("HTTP response exceeds configured byte bound"));
125    }
126    head.extend_from_slice(framing.as_bytes());
127    Ok(head)
128}
129
130fn validate_header_name(name: &str) -> io::Result<()> {
131    if name.is_empty()
132        || !name
133            .bytes()
134            .all(|byte| byte.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&byte))
135    {
136        return Err(invalid_input("HTTP response header name is not a token"));
137    }
138    Ok(())
139}
140
141fn validate_header_value(value: &str) -> io::Result<()> {
142    if value
143        .bytes()
144        .any(|byte| byte < 0x20 && byte != b'\t' || byte == 0x7f)
145    {
146        return Err(invalid_input(
147            "HTTP response header value contains a control byte",
148        ));
149    }
150    Ok(())
151}
152
153fn reason_phrase(status: u16) -> &'static str {
154    match status {
155        200 => "OK",
156        201 => "Created",
157        202 => "Accepted",
158        204 => "No Content",
159        206 => "Partial Content",
160        300 => "Multiple Choices",
161        301 => "Moved Permanently",
162        302 => "Found",
163        304 => "Not Modified",
164        307 => "Temporary Redirect",
165        308 => "Permanent Redirect",
166        400 => "Bad Request",
167        401 => "Unauthorized",
168        403 => "Forbidden",
169        404 => "Not Found",
170        405 => "Method Not Allowed",
171        409 => "Conflict",
172        413 => "Payload Too Large",
173        415 => "Unsupported Media Type",
174        422 => "Unprocessable Content",
175        429 => "Too Many Requests",
176        500 => "Internal Server Error",
177        501 => "Not Implemented",
178        503 => "Service Unavailable",
179        _ => "Unknown",
180    }
181}
182
183fn invalid_input(message: &str) -> io::Error {
184    io::Error::new(io::ErrorKind::InvalidInput, message)
185}
186
187fn invalid_data(message: &str) -> io::Error {
188    io::Error::new(io::ErrorKind::InvalidData, message)
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn response_rejects_transport_headers_and_controls() {
197        let mut response = HttpResponse::new(200, b"ok".to_vec()).expect("valid response");
198        assert!(response.set_header("Content-Length", "2").is_err());
199        assert!(
200            response
201                .set_header("X-Test", "ok\r\nInjected: true")
202                .is_err()
203        );
204        response.set_header("X-Test", "ok").expect("valid header");
205        assert!(response.set_header("x-test", "again").is_err());
206    }
207}