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}