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}