Skip to main content

moirai_http/server/
connection.rs

1//! Typestate connections and bounded request bodies.
2
3use std::io;
4
5use moirai_async::io::AsyncWriteExt;
6use moirai_async::net::TcpStream;
7use moirai_async::timer::timeout;
8
9use super::config::ServerConfig;
10use super::response::{HttpResponse, encode_response_head};
11use crate::request::{HttpRequestHead, read_request_head};
12
13/// A connection that has not read its request yet.
14#[derive(Debug)]
15pub struct AwaitingRequest;
16
17/// A connection that has read one request and may write one response.
18#[derive(Debug)]
19pub struct AwaitingResponse {
20    suppress_body: bool,
21}
22
23/// A one-shot HTTP connection whose typestate controls its lifecycle.
24pub struct HttpConnection<State> {
25    pub(super) stream: TcpStream,
26    pub(super) config: ServerConfig,
27    pub(super) prefix: Vec<u8>,
28    pub(super) state: State,
29}
30
31impl HttpConnection<AwaitingRequest> {
32    /// Read one bounded request and transition to [`AwaitingResponse`].
33    ///
34    /// The connection is consumed so a failed or timed-out read cannot be
35    /// retried at an ambiguous wire position.
36    ///
37    /// # Errors
38    /// Returns malformed, oversized, transfer-encoded, truncated, timed-out,
39    /// or transport failures.
40    pub async fn read_request(
41        mut self,
42    ) -> io::Result<(HttpRequest, HttpConnection<AwaitingResponse>)> {
43        let deadline = self.config.request_timeout;
44        match timeout(deadline, self.read_request_inner()).await {
45            Ok(Ok(request)) => {
46                let suppress_body = request.method() == "HEAD";
47                let connection = HttpConnection {
48                    stream: self.stream,
49                    config: self.config,
50                    prefix: Vec::new(),
51                    state: AwaitingResponse { suppress_body },
52                };
53                Ok((request, connection))
54            }
55            Ok(Err(error)) => Err(error),
56            Err(_) => Err(timed_out("HTTP request read")),
57        }
58    }
59
60    async fn read_request_inner(&mut self) -> io::Result<HttpRequest> {
61        let (head, prefix) = read_request_head(
62            &mut self.stream,
63            self.config.max_header_bytes,
64            self.config.max_header_count,
65        )
66        .await?;
67        if head.version() != "HTTP/1.1" {
68            return Err(invalid_data("HTTP server requires HTTP/1.1"));
69        }
70        self.prefix = prefix;
71        let body_length = request_body_length(&head, self.config.max_body_bytes)?;
72        let mut body = Vec::new();
73        body.try_reserve(body_length)
74            .map_err(|_| invalid_data("HTTP request body allocation exceeds available memory"))?;
75
76        if self.prefix.len() > body_length {
77            return Err(invalid_data(
78                "HTTP request contains bytes beyond Content-Length",
79            ));
80        }
81        if !self.prefix.is_empty() {
82            body.extend_from_slice(&self.prefix);
83        }
84
85        let mut remaining = body_length
86            .checked_sub(self.prefix.len())
87            .ok_or_else(|| invalid_data("HTTP request body length underflowed"))?;
88        while remaining != 0 {
89            let mut chunk = [0_u8; 8192];
90            let read_length = remaining.min(chunk.len());
91            let target = chunk
92                .get_mut(..read_length)
93                .ok_or_else(|| invalid_data("HTTP request read window exceeded its bound"))?;
94            let count = self.stream.read(target).await?;
95            if count == 0 {
96                return Err(io::Error::new(
97                    io::ErrorKind::UnexpectedEof,
98                    "HTTP request body ended before Content-Length",
99                ));
100            }
101            let bytes = chunk
102                .get(..count)
103                .ok_or_else(|| invalid_data("HTTP request read exceeded its bound"))?;
104            body.extend_from_slice(bytes);
105            remaining = remaining
106                .checked_sub(count)
107                .ok_or_else(|| invalid_data("HTTP request body length underflowed"))?;
108        }
109        self.prefix.clear();
110        Ok(HttpRequest { head, body })
111    }
112}
113
114impl HttpConnection<AwaitingResponse> {
115    /// Write one bounded response, flush it, and close the connection.
116    ///
117    /// The connection is consumed so a partial write or deadline cannot be
118    /// retried. A `HEAD` request keeps the declared `Content-Length` but omits
119    /// the response body on the wire.
120    ///
121    /// # Errors
122    /// Returns invalid response, oversized response, timed-out, or transport
123    /// failures.
124    pub async fn write_response(self, response: HttpResponse) -> io::Result<()> {
125        let deadline = self.config.request_timeout;
126        match timeout(deadline, self.write_response_inner(response)).await {
127            Ok(result) => result,
128            Err(_) => Err(timed_out("HTTP response write")),
129        }
130    }
131
132    async fn write_response_inner(self, response: HttpResponse) -> io::Result<()> {
133        let HttpConnection {
134            mut stream,
135            config,
136            state,
137            prefix: _,
138        } = self;
139        if !state.suppress_body
140            && matches!(response.status, 204 | 205 | 304)
141            && !response.body.is_empty()
142        {
143            return Err(invalid_data("HTTP status does not permit a response body"));
144        }
145        if response.headers.len() > config.max_header_count.saturating_sub(2) {
146            return Err(invalid_data(
147                "HTTP response header count exceeds configured bound",
148            ));
149        }
150        let head = encode_response_head(&response, config.max_response_bytes)?;
151        let total_size = head
152            .len()
153            .checked_add(response.body.len())
154            .ok_or_else(|| invalid_data("HTTP response size overflows its bound"))?;
155        if total_size > config.max_response_bytes {
156            return Err(invalid_data("HTTP response exceeds configured byte bound"));
157        }
158
159        stream.write_all(&head).await?;
160        if !state.suppress_body && !response.body.is_empty() {
161            stream.write_all(&response.body).await?;
162        }
163        stream.flush().await?;
164        stream.shutdown().await
165    }
166}
167
168/// A validated HTTP request with an owned bounded body.
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct HttpRequest {
171    head: HttpRequestHead,
172    body: Vec<u8>,
173}
174
175impl HttpRequest {
176    /// Return the request method.
177    #[must_use]
178    pub fn method(&self) -> &str {
179        self.head.method()
180    }
181
182    /// Return the origin-form request target.
183    #[must_use]
184    pub fn target(&self) -> &str {
185        self.head.target()
186    }
187
188    /// Return the HTTP version token.
189    #[must_use]
190    pub fn version(&self) -> &str {
191        self.head.version()
192    }
193
194    /// Return the first header value matching `name` case-insensitively.
195    #[must_use]
196    pub fn header(&self, name: &str) -> Option<&str> {
197        self.head.header(name)
198    }
199
200    /// Return all request headers in receive order.
201    #[must_use]
202    pub fn headers(&self) -> &[(String, String)] {
203        self.head.headers()
204    }
205
206    /// Return the complete request body.
207    #[must_use]
208    pub fn body(&self) -> &[u8] {
209        &self.body
210    }
211}
212
213fn request_body_length(head: &HttpRequestHead, max_body_bytes: usize) -> io::Result<usize> {
214    if head.header("transfer-encoding").is_some() {
215        return Err(invalid_data(
216            "transfer-encoded HTTP request bodies are not supported",
217        ));
218    }
219    let Some(value) = head.header("content-length") else {
220        return Ok(0);
221    };
222    let length = value
223        .parse::<usize>()
224        .map_err(|_| invalid_data("HTTP Content-Length is not a decimal byte count"))?;
225    if length > max_body_bytes {
226        return Err(invalid_data(
227            "HTTP request body exceeds configured byte bound",
228        ));
229    }
230    Ok(length)
231}
232
233fn invalid_data(message: &str) -> io::Error {
234    io::Error::new(io::ErrorKind::InvalidData, message)
235}
236
237fn timed_out(operation: &str) -> io::Error {
238    io::Error::new(io::ErrorKind::TimedOut, operation)
239}