1use zenith_api::CanonicalResponse;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum ResponseSerializeError {
25 CrlfInjection,
27 InvalidHeaderName,
29}
30
31impl std::fmt::Display for ResponseSerializeError {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 Self::CrlfInjection => write!(f, "CRLF injection detected in header"),
35 Self::InvalidHeaderName => write!(f, "invalid header name character"),
36 }
37 }
38}
39
40impl std::error::Error for ResponseSerializeError {}
41
42#[inline]
44fn reason_phrase(status: u16) -> &'static str {
45 match status {
46 100 => "Continue",
47 101 => "Switching Protocols",
48 200 => "OK",
49 201 => "Created",
50 202 => "Accepted",
51 203 => "Non-Authoritative Information",
52 204 => "No Content",
53 205 => "Reset Content",
54 206 => "Partial Content",
55 301 => "Moved Permanently",
56 302 => "Found",
57 303 => "See Other",
58 304 => "Not Modified",
59 307 => "Temporary Redirect",
60 308 => "Permanent Redirect",
61 400 => "Bad Request",
62 401 => "Unauthorized",
63 402 => "Payment Required",
64 403 => "Forbidden",
65 404 => "Not Found",
66 405 => "Method Not Allowed",
67 406 => "Not Acceptable",
68 407 => "Proxy Authentication Required",
69 408 => "Request Timeout",
70 409 => "Conflict",
71 410 => "Gone",
72 411 => "Length Required",
73 412 => "Precondition Failed",
74 413 => "Payload Too Large",
75 414 => "URI Too Long",
76 415 => "Unsupported Media Type",
77 416 => "Range Not Satisfiable",
78 417 => "Expectation Failed",
79 421 => "Misdirected Request",
80 422 => "Unprocessable Entity",
81 425 => "Too Early",
82 426 => "Upgrade Required",
83 428 => "Precondition Required",
84 429 => "Too Many Requests",
85 431 => "Request Header Fields Too Large",
86 451 => "Unavailable For Legal Reasons",
87 500 => "Internal Server Error",
88 501 => "Not Implemented",
89 502 => "Bad Gateway",
90 503 => "Service Unavailable",
91 504 => "Gateway Timeout",
92 505 => "HTTP Version Not Supported",
93 _ => "Unknown",
94 }
95}
96
97#[derive(Debug)]
101pub struct Http1ResponseEncoder;
102
103impl Http1ResponseEncoder {
104 pub fn encode(
118 response: &CanonicalResponse,
119 out: &mut Vec<u8>,
120 ) -> Result<usize, ResponseSerializeError> {
121 let start = out.len();
122
123 out.extend_from_slice(b"HTTP/1.1 ");
125 out.extend_from_slice(response.status_code.to_string().as_bytes());
126 out.push(b' ');
127 out.extend_from_slice(reason_phrase(response.status_code).as_bytes());
128 out.extend_from_slice(b"\r\n");
129
130 let mut has_content_length = false;
132 let mut has_transfer_encoding = false;
133
134 for header in response.headers_iter() {
136 let name = header.name_str();
137 let value = header.value_str();
138
139 if value.contains('\r') || value.contains('\n') {
141 return Err(ResponseSerializeError::CrlfInjection);
142 }
143
144 if !zenith_api::normalize::is_valid_header_name(name) {
146 return Err(ResponseSerializeError::InvalidHeaderName);
147 }
148
149 let lower = name.to_ascii_lowercase();
150 if lower == "content-length" {
151 has_content_length = true;
152 }
153 if lower == "transfer-encoding" {
154 has_transfer_encoding = true;
155 }
156
157 out.extend_from_slice(name.as_bytes());
158 out.extend_from_slice(b": ");
159 out.extend_from_slice(value.as_bytes());
160 out.extend_from_slice(b"\r\n");
161 }
162
163 let body = response.body();
168 let status = response.status_code;
169 let forbid_content_length = matches!(status, 100..=199) || status == 204 || status == 304;
170 if !forbid_content_length && !has_content_length && !has_transfer_encoding {
171 out.extend_from_slice(b"content-length: ");
172 out.extend_from_slice(body.len().to_string().as_bytes());
173 out.extend_from_slice(b"\r\n");
174 }
175
176 out.extend_from_slice(b"\r\n");
178
179 if !forbid_content_length && !body.is_empty() {
182 out.extend_from_slice(body);
183 }
184
185 Ok(out.len() - start)
186 }
187
188 #[inline]
190 pub fn encode_to_vec(response: &CanonicalResponse) -> Result<Vec<u8>, ResponseSerializeError> {
191 let mut out = Vec::with_capacity(256 + response.body().len());
192 Http1ResponseEncoder::encode(response, &mut out)?;
193 Ok(out)
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use zenith_api::CanonicalResponse;
201
202 #[test]
203 fn test_encode_simple_response() {
204 let mut resp = CanonicalResponse::new(200);
205 resp.add_header(b"content-type", b"text/plain").unwrap();
206 resp.set_body(b"Hello, World!");
207
208 let mut out = Vec::new();
209 let n = Http1ResponseEncoder::encode(&resp, &mut out).unwrap();
210 let s = String::from_utf8(out).unwrap();
211
212 assert!(s.starts_with("HTTP/1.1 200 OK\r\n"));
213 assert!(s.contains("content-type: text/plain\r\n"));
214 assert!(s.contains("content-length: 13\r\n"));
215 assert!(s.ends_with("\r\nHello, World!"));
216 assert_eq!(n, s.len());
217 }
218
219 #[test]
220 fn test_encode_no_body() {
221 let resp = CanonicalResponse::new(204);
222 let mut out = Vec::new();
223 Http1ResponseEncoder::encode(&resp, &mut out).unwrap();
224 let s = String::from_utf8(out).unwrap();
225
226 assert!(s.starts_with("HTTP/1.1 204 No Content\r\n"));
227 assert!(s.ends_with("\r\n\r\n"));
228 assert!(!s.contains("content-length"));
230 }
231
232 #[test]
233 fn test_encode_with_explicit_content_length() {
234 let mut resp = CanonicalResponse::new(200);
235 resp.add_header(b"content-length", b"42").unwrap();
236 resp.set_body(b"Hello");
237
238 let mut out = Vec::new();
239 Http1ResponseEncoder::encode(&resp, &mut out).unwrap();
240 let s = String::from_utf8(out).unwrap();
241
242 let cl_count = s.matches("content-length").count();
244 assert_eq!(cl_count, 1);
245 assert!(s.contains("content-length: 42\r\n"));
246 }
247
248 #[test]
249 fn test_encode_crlf_injection_blocked() {
250 let mut resp = CanonicalResponse::new(200);
251 let add_result = resp.add_header(b"x-evil", b"val\r\nInjected: yes");
253 assert!(add_result.is_err(), "add_header must reject CRLF in value");
254
255 let mut out = Vec::new();
257 let result = Http1ResponseEncoder::encode(&resp, &mut out);
258 assert!(result.is_ok(), "encode should succeed on clean response");
259 }
260
261 #[test]
262 fn test_encode_reason_phrases() {
263 for (code, phrase) in &[
264 (200u16, "OK"),
265 (404, "Not Found"),
266 (500, "Internal Server Error"),
267 (301, "Moved Permanently"),
268 (429, "Too Many Requests"),
269 (451, "Unavailable For Legal Reasons"),
270 ] {
271 let resp = CanonicalResponse::new(*code);
272 let out = Http1ResponseEncoder::encode_to_vec(&resp).unwrap();
273 let s = String::from_utf8(out).unwrap();
274 assert!(s.starts_with(&format!("HTTP/1.1 {code} {phrase}\r\n")));
275 }
276 }
277
278 #[test]
279 fn test_encode_unknown_status() {
280 let resp = CanonicalResponse::new(599);
281 let out = Http1ResponseEncoder::encode_to_vec(&resp).unwrap();
282 let s = String::from_utf8(out).unwrap();
283 assert!(s.starts_with("HTTP/1.1 599 Unknown\r\n"));
284 }
285
286 #[test]
287 fn test_encode_to_vec() {
288 let mut resp = CanonicalResponse::new(200);
289 resp.set_body(b"test");
290 let out = Http1ResponseEncoder::encode_to_vec(&resp).unwrap();
291 assert!(!out.is_empty());
292 assert!(out.windows(4).any(|w| w == b"test"));
293 }
294
295 #[test]
296 fn test_encode_multiple_headers() {
297 let mut resp = CanonicalResponse::new(200);
298 resp.add_header(b"x-custom-1", b"value1").unwrap();
299 resp.add_header(b"x-custom-2", b"value2").unwrap();
300 resp.add_header(b"server", b"Zenith/1.0").unwrap();
301 resp.set_body(b"OK");
302
303 let s = String::from_utf8(Http1ResponseEncoder::encode_to_vec(&resp).unwrap()).unwrap();
304 assert!(s.contains("x-custom-1: value1\r\n"));
305 assert!(s.contains("x-custom-2: value2\r\n"));
306 assert!(s.contains("server: Zenith/1.0\r\n"));
307 assert!(s.ends_with("\r\nOK"));
308 }
309
310 #[test]
311 fn test_encode_empty_body_with_content_type() {
312 let mut resp = CanonicalResponse::new(200);
313 resp.add_header(b"content-type", b"application/json").unwrap();
314 let s = String::from_utf8(Http1ResponseEncoder::encode_to_vec(&resp).unwrap()).unwrap();
316 assert!(s.contains("content-type: application/json\r\n"));
317 assert!(
319 s.contains("content-length: 0"),
320 "empty-body 200 response must include Content-Length: 0 for keep-alive compliance. Raw: {s}"
321 );
322 }
323
324 #[test]
325 fn test_reason_phrase_all_variants() {
326 let codes = [100, 101, 200, 201, 204, 206, 301, 302, 304, 400, 401, 403, 404, 500];
327 for &code in &codes {
328 let resp = CanonicalResponse::new(code);
329 let s = String::from_utf8(Http1ResponseEncoder::encode_to_vec(&resp).unwrap()).unwrap();
330 assert!(!s.contains("Unknown"), "status {code} should have known reason phrase");
331 }
332 }
333}