Skip to main content

http_handle/
response.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (c) 2026 Sebastien Rousseau
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::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    /// Sends the response over the provided `Write` stream.
112    ///
113    /// This method writes the HTTP status line, headers, and body to the stream, ensuring
114    /// the client receives the complete response.
115    ///
116    /// # Arguments
117    ///
118    /// * `stream` - A mutable reference to any stream that implements `Write`.
119    ///
120    /// # Examples
121    ///
122    /// ```rust
123    /// use http_handle::response::Response;
124    /// use std::io::Cursor;
125    ///
126    /// let mut response = Response::new(200, "OK", b"hello".to_vec());
127    /// response.add_header("Content-Type", "text/plain");
128    ///
129    /// let mut out = Cursor::new(Vec::<u8>::new());
130    /// response.send(&mut out).expect("response write should succeed");
131    /// assert!(!out.get_ref().is_empty());
132    /// ```
133    ///
134    /// # Errors
135    ///
136    /// Returns `Err` when writing headers or body to the output stream fails.
137    ///
138    /// # Panics
139    ///
140    /// This function does not intentionally panic.
141    #[doc(alias = "serialize")]
142    #[doc(alias = "write response")]
143    pub fn send<W: Write>(
144        &self,
145        stream: &mut W,
146    ) -> Result<(), ServerError> {
147        let mut has_content_length = false;
148        let mut has_connection = false;
149
150        write!(
151            stream,
152            "HTTP/1.1 {} {}\r\n",
153            self.status_code, self.status_text
154        )?;
155
156        for (name, value) in &self.headers {
157            if name.eq_ignore_ascii_case("content-length") {
158                has_content_length = true;
159            }
160            if name.eq_ignore_ascii_case("connection") {
161                has_connection = true;
162            }
163            write!(stream, "{}: {}\r\n", name, value)?;
164        }
165
166        if !has_content_length {
167            write!(stream, "Content-Length: {}\r\n", self.body.len())?;
168        }
169        if !has_connection {
170            write!(stream, "Connection: close\r\n")?;
171        }
172
173        write!(stream, "\r\n")?;
174        stream.write_all(&self.body)?;
175        stream.flush()?;
176
177        Ok(())
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use std::io::{self, Cursor, Write};
185
186    /// Test case for the `Response::new` method.
187    #[test]
188    fn test_response_new() {
189        let status_code = 200;
190        let status_text = "OK";
191        let body = b"Hello, world!".to_vec();
192        let response =
193            Response::new(status_code, status_text, body.clone());
194
195        assert_eq!(response.status_code, status_code);
196        assert_eq!(response.status_text, status_text.to_string());
197        assert!(response.headers.is_empty());
198        assert_eq!(response.body, body);
199    }
200
201    /// Test case for the `Response::add_header` method.
202    #[test]
203    fn test_response_add_header() {
204        let mut response = Response::new(200, "OK", vec![]);
205        response.add_header("Content-Type", "text/html");
206
207        assert_eq!(response.headers.len(), 1);
208        assert_eq!(
209            response.headers[0],
210            ("Content-Type".to_string(), "text/html".to_string())
211        );
212    }
213
214    /// A mock implementation of `Write` to simulate writing the response without actual network operations.
215    struct MockTcpStream {
216        buffer: Cursor<Vec<u8>>,
217    }
218
219    impl MockTcpStream {
220        fn new() -> Self {
221            MockTcpStream {
222                buffer: Cursor::new(Vec::new()),
223            }
224        }
225
226        fn get_written_data(&self) -> Vec<u8> {
227            self.buffer.clone().into_inner()
228        }
229    }
230
231    impl Write for MockTcpStream {
232        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
233            self.buffer.write(buf)
234        }
235
236        fn flush(&mut self) -> io::Result<()> {
237            self.buffer.flush()
238        }
239    }
240
241    /// Test case for the `Response::send` method.
242    #[test]
243    fn test_response_send() {
244        let mut response =
245            Response::new(200, "OK", b"Hello, world!".to_vec());
246        response.add_header("Content-Type", "text/plain");
247
248        let mut mock_stream = MockTcpStream::new();
249        let result = response.send(&mut mock_stream);
250
251        assert!(result.is_ok());
252
253        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!";
254        let written_data = mock_stream.get_written_data();
255
256        assert_eq!(written_data, expected_output);
257    }
258
259    /// Test case for `Response::send` when there is an error during writing.
260    #[test]
261    fn test_response_send_error() {
262        let mut response =
263            Response::new(200, "OK", b"Hello, world!".to_vec());
264        response.add_header("Content-Type", "text/plain");
265
266        struct FailingStream;
267
268        impl Write for FailingStream {
269            fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
270                Err(io::Error::other("write error"))
271            }
272
273            fn flush(&mut self) -> io::Result<()> {
274                Ok(())
275            }
276        }
277
278        let mut failing_stream = FailingStream;
279        let result = response.send(&mut failing_stream);
280        failing_stream.flush().expect("flush");
281
282        assert!(result.is_err());
283    }
284}