Skip to main content

http_handle/
response.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (c) 2023 - 2026 HTTP Handle
3
4// src/response.rs
5
6//! HTTP response construction and serialization.
7//!
8//! Use this module to build status lines, headers, and body payloads and emit them to any
9//! writable stream with stable HTTP/1.1 framing defaults.
10
11use crate::error::ServerError;
12use serde::{Deserialize, Serialize};
13use std::io::{BufWriter, Write};
14
15/// Represents an HTTP response payload and metadata.
16///
17/// You create this type on the response path, add headers, and serialize it to any
18/// `Write` sink (for example `TcpStream` or an in-memory buffer in tests).
19///
20/// # Examples
21///
22/// ```rust
23/// use http_handle::response::Response;
24///
25/// let response = Response::new(200, "OK", b"hello".to_vec());
26/// assert_eq!(response.status_code, 200);
27/// ```
28///
29/// # Panics
30///
31/// This type does not panic on construction.
32#[doc(alias = "http response")]
33#[derive(
34    Clone, Debug, PartialEq, Eq, Hash, Default, Serialize, Deserialize,
35)]
36pub struct Response {
37    /// The HTTP status code (e.g., 200 for OK, 404 for Not Found).
38    pub status_code: u16,
39
40    /// The HTTP status text associated with the status code (e.g., "OK", "Not Found").
41    pub status_text: String,
42
43    /// A list of headers in the response, each represented as a tuple containing the header
44    /// name and its corresponding value.
45    pub headers: Vec<(String, String)>,
46
47    /// The body of the response, represented as a vector of bytes.
48    pub body: Vec<u8>,
49}
50
51impl Response {
52    /// Creates a response with status, reason, and body bytes.
53    ///
54    /// The headers are initialized as an empty list and can be added later using the `add_header` method.
55    ///
56    /// # Arguments
57    ///
58    /// * `status_code` - The HTTP status code for the response.
59    /// * `status_text` - The status text corresponding to the status code.
60    /// * `body` - The body of the response, represented as a vector of bytes.
61    ///
62    /// # Examples
63    ///
64    /// ```rust
65    /// use http_handle::response::Response;
66    ///
67    /// let response = Response::new(204, "NO CONTENT", Vec::new());
68    /// assert_eq!(response.status_code, 204);
69    /// ```
70    ///
71    /// # Panics
72    ///
73    /// This function does not panic.
74    #[doc(alias = "constructor")]
75    pub fn new(
76        status_code: u16,
77        status_text: &str,
78        body: Vec<u8>,
79    ) -> Self {
80        Response {
81            status_code,
82            status_text: status_text.to_string(),
83            headers: Vec::new(),
84            body,
85        }
86    }
87
88    /// Adds a header to the response.
89    ///
90    /// This method allows you to add custom headers to the response, which will be included
91    /// in the HTTP response when it is sent to the client.
92    ///
93    /// # Examples
94    ///
95    /// ```rust
96    /// use http_handle::response::Response;
97    ///
98    /// let mut response = Response::new(200, "OK", Vec::new());
99    /// response.add_header("Content-Type", "text/plain");
100    /// assert_eq!(response.headers.len(), 1);
101    /// ```
102    ///
103    /// # Panics
104    ///
105    /// This function does not panic.
106    #[doc(alias = "set header")]
107    pub fn add_header(&mut self, name: &str, value: &str) {
108        self.headers.push((name.to_string(), value.to_string()));
109    }
110
111    /// Sets the `Connection` header to `value`, replacing any existing
112    /// `Connection` header (case-insensitive match).
113    ///
114    /// Used by the keep-alive loop to write the authoritative
115    /// connection lifecycle decision over whatever upstream policies
116    /// may have set. Operates on a single header name so the linear
117    /// retain is bounded by `headers.len()`.
118    ///
119    /// # Examples
120    ///
121    /// ```rust
122    /// use http_handle::response::Response;
123    ///
124    /// let mut r = Response::new(200, "OK", Vec::new());
125    /// r.add_header("Connection", "close");
126    /// r.set_connection_header("keep-alive");
127    /// assert!(r.headers.iter().any(|(n, v)| {
128    ///     n.eq_ignore_ascii_case("Connection") && v == "keep-alive"
129    /// }));
130    /// ```
131    pub fn set_connection_header(&mut self, value: &str) {
132        self.headers.retain(|(name, _)| {
133            !name.eq_ignore_ascii_case("connection")
134        });
135        self.headers
136            .push(("Connection".to_string(), value.to_string()));
137    }
138
139    /// Sends the response over the provided `Write` stream.
140    ///
141    /// This method writes the HTTP status line, headers, and body to the stream, ensuring
142    /// the client receives the complete response.
143    ///
144    /// # Arguments
145    ///
146    /// * `stream` - A mutable reference to any stream that implements `Write`.
147    ///
148    /// # Examples
149    ///
150    /// ```rust
151    /// use http_handle::response::Response;
152    /// use std::io::Cursor;
153    ///
154    /// let mut response = Response::new(200, "OK", b"hello".to_vec());
155    /// response.add_header("Content-Type", "text/plain");
156    ///
157    /// let mut out = Cursor::new(Vec::<u8>::new());
158    /// response.send(&mut out).expect("response write should succeed");
159    /// assert!(!out.get_ref().is_empty());
160    /// ```
161    ///
162    /// # Errors
163    ///
164    /// Returns `Err` when writing headers or body to the output stream fails.
165    ///
166    /// # Panics
167    ///
168    /// This function does not intentionally panic.
169    #[doc(alias = "serialize")]
170    #[doc(alias = "write response")]
171    pub fn send<W: Write>(
172        &self,
173        stream: &mut W,
174    ) -> Result<(), ServerError> {
175        // Coalesce status line, headers, and trailer CRLF into a single
176        // buffered flush. Prior implementation emitted one write() syscall
177        // per header field; for a typical 5-header response that collapses
178        // 8+ syscalls into 1–2.
179        let mut w = BufWriter::with_capacity(4096, stream);
180
181        let mut has_content_length = false;
182        let mut has_connection = false;
183
184        write!(
185            w,
186            "HTTP/1.1 {} {}\r\n",
187            self.status_code, self.status_text
188        )?;
189
190        for (name, value) in &self.headers {
191            if name.eq_ignore_ascii_case("content-length") {
192                has_content_length = true;
193            }
194            if name.eq_ignore_ascii_case("connection") {
195                has_connection = true;
196            }
197            write!(w, "{}: {}\r\n", name, value)?;
198        }
199
200        if !has_content_length {
201            write!(w, "Content-Length: {}\r\n", self.body.len())?;
202        }
203        if !has_connection {
204            w.write_all(b"Connection: close\r\n")?;
205        }
206
207        w.write_all(b"\r\n")?;
208        w.write_all(&self.body)?;
209        w.flush()?;
210
211        Ok(())
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use std::io::{self, Cursor, Write};
219
220    /// Test case for the `Response::new` method.
221    #[test]
222    fn test_response_new() {
223        let status_code = 200;
224        let status_text = "OK";
225        let body = b"Hello, world!".to_vec();
226        let response =
227            Response::new(status_code, status_text, body.clone());
228
229        assert_eq!(response.status_code, status_code);
230        assert_eq!(response.status_text, status_text.to_string());
231        assert!(response.headers.is_empty());
232        assert_eq!(response.body, body);
233    }
234
235    /// Test case for the `Response::add_header` method.
236    #[test]
237    fn test_response_add_header() {
238        let mut response = Response::new(200, "OK", vec![]);
239        response.add_header("Content-Type", "text/html");
240
241        assert_eq!(response.headers.len(), 1);
242        assert_eq!(
243            response.headers[0],
244            ("Content-Type".to_string(), "text/html".to_string())
245        );
246    }
247
248    /// A mock implementation of `Write` to simulate writing the response without actual network operations.
249    struct MockTcpStream {
250        buffer: Cursor<Vec<u8>>,
251    }
252
253    impl MockTcpStream {
254        fn new() -> Self {
255            MockTcpStream {
256                buffer: Cursor::new(Vec::new()),
257            }
258        }
259
260        fn get_written_data(&self) -> Vec<u8> {
261            self.buffer.clone().into_inner()
262        }
263    }
264
265    impl Write for MockTcpStream {
266        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
267            self.buffer.write(buf)
268        }
269
270        fn flush(&mut self) -> io::Result<()> {
271            self.buffer.flush()
272        }
273    }
274
275    /// Test case for the `Response::send` method.
276    #[test]
277    fn test_response_send() {
278        let mut response =
279            Response::new(200, "OK", b"Hello, world!".to_vec());
280        response.add_header("Content-Type", "text/plain");
281
282        let mut mock_stream = MockTcpStream::new();
283        let result = response.send(&mut mock_stream);
284
285        assert!(result.is_ok());
286
287        let expected_output = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\nConnection: close\r\n\r\nHello, world!";
288        let written_data = mock_stream.get_written_data();
289
290        assert_eq!(written_data, expected_output);
291    }
292
293    /// Test case for `Response::send` when there is an error during writing.
294    #[test]
295    fn test_response_send_error() {
296        let mut response =
297            Response::new(200, "OK", b"Hello, world!".to_vec());
298        response.add_header("Content-Type", "text/plain");
299
300        struct FailingStream;
301
302        impl Write for FailingStream {
303            fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
304                Err(io::Error::other("write error"))
305            }
306
307            fn flush(&mut self) -> io::Result<()> {
308                Ok(())
309            }
310        }
311
312        let mut failing_stream = FailingStream;
313        let result = response.send(&mut failing_stream);
314        failing_stream.flush().expect("flush");
315
316        assert!(result.is_err());
317    }
318
319    /// Forces the status-line `write!` to overflow the internal
320    /// `BufWriter` (4096 B capacity) mid-call so the underlying
321    /// `FailingStream::write` is invoked and the `?` on the status
322    /// line fires — that's the only way to cover the early-return
323    /// path; smaller writes sit in the buffer and surface only on
324    /// the trailing `flush()`.
325    #[test]
326    fn test_response_send_propagates_status_line_overflow_error() {
327        let huge_status = "X".repeat(8 * 1024);
328        let response = Response::new(200, &huge_status, b"".to_vec());
329
330        struct FailingStream;
331        impl Write for FailingStream {
332            fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
333                Err(io::Error::other("write error"))
334            }
335            fn flush(&mut self) -> io::Result<()> {
336                Ok(())
337            }
338        }
339
340        let mut sink = FailingStream;
341        let err = response.send(&mut sink).expect_err("must fail");
342        assert!(err.to_string().contains("write error"));
343        // Exercise the impl's flush() arm so it carries coverage too.
344        sink.flush().expect("flush always Ok");
345    }
346}