Skip to main content

rws_http/response/
mod.rs

1//! HTTP responses: [`Response`], its writers and the upstream-response parser.
2//!
3//! A response has a status, headers and a body as a list of
4//! [`ContentRange`](crate::range::ContentRange)s (one, or several for a
5//! `multipart/byteranges` range response), or a `stream_file` / `stream_pipe`
6//! for bodies too large to buffer. `Response::json` / `Response::text` build the
7//! common cases. The writers (`generate_response`, `generate_response_parts`,
8//! …) add `Content-Type`, `Content-Length` and `Content-Range`;
9//! [`Response::parse`] reads a raw upstream response (used by the proxy).
10//! Examples: `src/response/example/`.
11
12#[cfg(test)]
13mod tests;
14#[cfg(test)]
15mod example;
16#[cfg(test)]
17mod reference_parser;
18
19use crate::ext::decimal::Decimal;
20use std::io;
21use std::io::{BufRead, Cursor, Read};
22use crate::body::multipart_form_data::FormMultipartData;
23use crate::core::New;
24use crate::header::Header;
25use crate::ext::string_ext::StringExt;
26use crate::http::{HTTP, VERSION};
27use crate::mime_type::MimeType;
28use crate::range::{ContentRange, Range};
29use crate::request::{METHOD, Request};
30use crate::symbol::SYMBOL;
31
32#[derive(PartialEq, Eq, Clone, Debug)]
33pub struct Error {
34    pub status_code_reason_phrase: &'static StatusCodeReasonPhrase,
35    pub message: String,
36}
37
38/// An HTTP response. Build one inside [`Controller::process`] and return it.
39///
40/// Set `status_code` and `reason_phrase` from [`STATUS_CODE_REASON_PHRASE`],
41/// push body content via [`Range::get_content_range`] into `content_range_list`,
42/// and add any extra headers to `headers`.
43///
44/// For large files, set `stream_file` to the absolute file path instead of loading
45/// the body into `content_range_list`. The server will stream it with
46/// `Transfer-Encoding: chunked` so memory usage stays constant regardless of file size.
47///
48/// For proxy passthrough (SSE, AI token streams, large downloads), set `stream_pipe`
49/// to a boxed `Read` source instead. The server streams its output with
50/// `Transfer-Encoding: chunked` immediately after sending the response headers.
51pub struct Response {
52    /// HTTP version string, e.g. `"HTTP/1.1"`.
53    pub http_version: String,
54    /// Numeric status code, e.g. `200`. Set from [`STATUS_CODE_REASON_PHRASE`].
55    pub status_code: i16,
56    /// Reason phrase, e.g. `"OK"`. Set from [`STATUS_CODE_REASON_PHRASE`].
57    pub reason_phrase: String,
58    /// Response headers. Pre-populated with standard headers by [`Header::get_header_list`].
59    pub headers: Vec<Header>,
60    /// Response body as a list of content ranges (supports multipart and range responses).
61    pub content_range_list: Vec<ContentRange>,
62    /// If set, the server streams this file with `Transfer-Encoding: chunked`
63    /// instead of using `content_range_list`. Use for files larger than ~8 MB.
64    pub stream_file: Option<String>,
65    /// If set, the server pipes bytes from this reader with `Transfer-Encoding: chunked`
66    /// immediately after sending the response headers. Takes precedence over
67    /// `content_range_list` and `stream_file`. The reader is consumed and not cloned.
68    pub stream_pipe: Option<Box<dyn std::io::Read + Send>>,
69    /// Header fields sent after the body (HTTP/2 and HTTP/3; HTTP/1.1 drops
70    /// them) — gRPC's `grpc-status` and `grpc-message`. A streamed body's
71    /// producer fills the slot before it ends the stream. Since 18.3.0.
72    pub trailers: Option<Trailers>,
73}
74
75/// Trailer fields shared between whoever produces a body and the connection
76/// writer that sends them after it (see [`Response::trailers`]). Clones share
77/// the one slot.
78#[derive(Clone, Default, Debug)]
79pub struct Trailers(std::sync::Arc<std::sync::Mutex<Vec<Header>>>);
80
81impl Trailers {
82    /// An empty slot.
83    pub fn new() -> Self {
84        Trailers::default()
85    }
86
87    /// Trailers already known, e.g. for a buffered body.
88    pub fn with(fields: Vec<Header>) -> Self {
89        let trailers = Trailers::new();
90        trailers.set(fields);
91        trailers
92    }
93
94    /// Replace the fields; call before the body's last byte is produced.
95    pub fn set(&self, fields: Vec<Header>) {
96        *self.0.lock().unwrap_or_else(|e| e.into_inner()) = fields;
97    }
98
99    /// The fields set so far.
100    pub fn get(&self) -> Vec<Header> {
101        self.0.lock().unwrap_or_else(|e| e.into_inner()).clone()
102    }
103}
104
105impl Clone for Response {
106    fn clone(&self) -> Self {
107        Response {
108            http_version: self.http_version.clone(),
109            status_code: self.status_code,
110            reason_phrase: self.reason_phrase.clone(),
111            headers: self.headers.clone(),
112            content_range_list: self.content_range_list.clone(),
113            stream_file: self.stream_file.clone(),
114            stream_pipe: None,
115            trailers: self.trailers.clone(),
116        }
117    }
118}
119
120impl PartialEq for Response {
121    fn eq(&self, other: &Self) -> bool {
122        self.http_version == other.http_version
123            && self.status_code == other.status_code
124            && self.reason_phrase == other.reason_phrase
125            && self.headers == other.headers
126            && self.content_range_list == other.content_range_list
127            && self.stream_file == other.stream_file
128    }
129}
130
131impl Eq for Response {}
132
133impl std::fmt::Debug for Response {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        f.debug_struct("Response")
136            .field("http_version", &self.http_version)
137            .field("status_code", &self.status_code)
138            .field("reason_phrase", &self.reason_phrase)
139            .field("headers", &self.headers)
140            .field("content_range_list", &self.content_range_list)
141            .field("stream_file", &self.stream_file)
142            .field("stream_pipe", &self.stream_pipe.as_ref().map(|_| "<stream>"))
143            .finish()
144    }
145}
146
147#[derive(PartialEq, Eq, Clone, Debug)]
148pub struct StatusCodeReasonPhrase {
149    pub status_code: &'static i16,
150    pub reason_phrase: &'static str,
151}
152
153#[derive(PartialEq, Eq, Clone, Debug)]
154pub struct ResponseStatusCodeReasonPhrase {
155    pub n100_continue: &'static StatusCodeReasonPhrase,
156    pub n101_switching_protocols: &'static StatusCodeReasonPhrase,
157    pub n102_processing: &'static StatusCodeReasonPhrase,
158    pub n103_early_hints: &'static StatusCodeReasonPhrase,
159    pub n200_ok: &'static StatusCodeReasonPhrase,
160    pub n201_created: &'static StatusCodeReasonPhrase,
161    pub n202_accepted: &'static StatusCodeReasonPhrase,
162    pub n203_non_authoritative_information: &'static StatusCodeReasonPhrase,
163    pub n204_no_content: &'static StatusCodeReasonPhrase,
164    pub n205_reset_content: &'static StatusCodeReasonPhrase,
165    pub n206_partial_content: &'static StatusCodeReasonPhrase,
166    pub n207_multi_status: &'static StatusCodeReasonPhrase,
167    pub n208_already_reported: &'static StatusCodeReasonPhrase,
168    pub n226_im_used: &'static StatusCodeReasonPhrase,
169    pub n300_multiple_choices: &'static StatusCodeReasonPhrase,
170    pub n301_moved_permanently: &'static StatusCodeReasonPhrase,
171    pub n302_found: &'static StatusCodeReasonPhrase,
172    pub n303_see_other: &'static StatusCodeReasonPhrase,
173    pub n304_not_modified: &'static StatusCodeReasonPhrase,
174    pub n307_temporary_redirect: &'static StatusCodeReasonPhrase,
175    pub n308_permanent_redirect: &'static StatusCodeReasonPhrase,
176    pub n400_bad_request: &'static StatusCodeReasonPhrase,
177    pub n401_unauthorized: &'static StatusCodeReasonPhrase,
178    pub n402_payment_required: &'static StatusCodeReasonPhrase,
179    pub n403_forbidden: &'static StatusCodeReasonPhrase,
180    pub n404_not_found: &'static StatusCodeReasonPhrase,
181    pub n405_method_not_allowed: &'static StatusCodeReasonPhrase,
182    pub n406_not_acceptable: &'static StatusCodeReasonPhrase,
183    pub n407_proxy_authentication_required: &'static StatusCodeReasonPhrase,
184    pub n408_request_timeout: &'static StatusCodeReasonPhrase,
185    pub n409_conflict: &'static StatusCodeReasonPhrase,
186    pub n410_gone: &'static StatusCodeReasonPhrase,
187    pub n411_length_required: &'static StatusCodeReasonPhrase,
188    pub n412_precondition_failed: &'static StatusCodeReasonPhrase,
189    pub n413_payload_too_large: &'static StatusCodeReasonPhrase,
190    pub n414_uri_too_long: &'static StatusCodeReasonPhrase,
191    pub n415_unsupported_media_type: &'static StatusCodeReasonPhrase,
192    pub n416_range_not_satisfiable: &'static StatusCodeReasonPhrase,
193    pub n417_expectation_failed: &'static StatusCodeReasonPhrase,
194    pub n418_im_a_teapot: &'static StatusCodeReasonPhrase,
195    pub n421_misdirected_request: &'static StatusCodeReasonPhrase,
196    pub n422_unprocessable_entity: &'static StatusCodeReasonPhrase,
197    pub n423_locked: &'static StatusCodeReasonPhrase,
198    pub n424_failed_dependency: &'static StatusCodeReasonPhrase,
199    pub n425_too_early: &'static StatusCodeReasonPhrase,
200    pub n426_upgrade_required: &'static StatusCodeReasonPhrase,
201    pub n428_precondition_required: &'static StatusCodeReasonPhrase,
202    pub n429_too_many_requests: &'static StatusCodeReasonPhrase,
203    pub n431_request_header_fields_too_large: &'static StatusCodeReasonPhrase,
204    pub n451_unavailable_for_legal_reasons: &'static StatusCodeReasonPhrase,
205    pub n500_internal_server_error: &'static StatusCodeReasonPhrase,
206    pub n501_not_implemented: &'static StatusCodeReasonPhrase,
207    pub n502_bad_gateway: &'static StatusCodeReasonPhrase,
208    pub n503_service_unavailable: &'static StatusCodeReasonPhrase,
209    pub n504_gateway_timeout: &'static StatusCodeReasonPhrase,
210    pub n505_http_version_not_supported: &'static StatusCodeReasonPhrase,
211    pub n506_variant_also_negotiates: &'static StatusCodeReasonPhrase,
212    pub n507_insufficient_storage: &'static StatusCodeReasonPhrase,
213    pub n508_loop_detected: &'static StatusCodeReasonPhrase,
214    pub n510_not_extended: &'static StatusCodeReasonPhrase,
215    pub n511_network_authentication_required: &'static StatusCodeReasonPhrase,
216}
217
218pub const STATUS_CODE_REASON_PHRASE: ResponseStatusCodeReasonPhrase = ResponseStatusCodeReasonPhrase {
219    n100_continue: &StatusCodeReasonPhrase { status_code: &100, reason_phrase: "Continue" },
220    n101_switching_protocols: &StatusCodeReasonPhrase { status_code: &101, reason_phrase: "Switching Protocols" },
221    n102_processing: &StatusCodeReasonPhrase { status_code: &102, reason_phrase: "Processing" },
222    n103_early_hints: &StatusCodeReasonPhrase { status_code: &103, reason_phrase: "Early Hints" },
223    n200_ok: &StatusCodeReasonPhrase {
224        status_code: &200,
225        reason_phrase: "OK"
226    },
227
228    n201_created: &StatusCodeReasonPhrase { status_code: &201, reason_phrase: "Created" },
229    n202_accepted: &StatusCodeReasonPhrase { status_code: &202, reason_phrase: "Accepted" },
230    n203_non_authoritative_information: &StatusCodeReasonPhrase { status_code: &203, reason_phrase: "Non Authoritative Information" },
231    n204_no_content: &StatusCodeReasonPhrase {
232        status_code: &204,
233        reason_phrase: "No Content"
234    },
235
236    n205_reset_content: &StatusCodeReasonPhrase { status_code: &205, reason_phrase: "Reset Content" },
237    n206_partial_content: &StatusCodeReasonPhrase {
238        status_code: &206,
239        reason_phrase: "Partial Content"
240    },
241
242    n207_multi_status: &StatusCodeReasonPhrase { status_code: &207, reason_phrase: "Multi-Status" },
243    n208_already_reported: &StatusCodeReasonPhrase { status_code: &208, reason_phrase: "Already Reported" },
244    n226_im_used: &StatusCodeReasonPhrase { status_code: &226, reason_phrase: "IM Used" },
245    n300_multiple_choices: &StatusCodeReasonPhrase { status_code: &300, reason_phrase: "Multiple Choices" },
246    n301_moved_permanently: &StatusCodeReasonPhrase { status_code: &301, reason_phrase: "Moved Permanently" },
247    n302_found: &StatusCodeReasonPhrase { status_code: &302, reason_phrase: "Found" },
248    n303_see_other: &StatusCodeReasonPhrase { status_code: &303, reason_phrase: "See Other" },
249    n304_not_modified: &StatusCodeReasonPhrase { status_code: &304, reason_phrase: "Not Modified" },
250    n307_temporary_redirect: &StatusCodeReasonPhrase { status_code: &307, reason_phrase: "Temporary Redirect" },
251    n308_permanent_redirect: &StatusCodeReasonPhrase { status_code: &308, reason_phrase: "Permanent Redirect" },
252    n400_bad_request: &StatusCodeReasonPhrase {
253        status_code: &400,
254        reason_phrase: "Bad Request"
255    },
256
257    n401_unauthorized: &StatusCodeReasonPhrase { status_code: &401, reason_phrase: "Unauthorized" },
258    n402_payment_required: &StatusCodeReasonPhrase { status_code: &402, reason_phrase: "Payment Required" },
259    n403_forbidden: &StatusCodeReasonPhrase { status_code: &403, reason_phrase: "Forbidden" },
260    n404_not_found: &StatusCodeReasonPhrase {
261        status_code: &404,
262        reason_phrase: "Not Found"
263    },
264
265    n405_method_not_allowed: &StatusCodeReasonPhrase { status_code: &405, reason_phrase: "Method Not Allowed" },
266    n406_not_acceptable: &StatusCodeReasonPhrase { status_code: &406, reason_phrase: "Not Acceptable" },
267    n407_proxy_authentication_required: &StatusCodeReasonPhrase { status_code: &407, reason_phrase: "Proxy Authentication Required" },
268    n408_request_timeout: &StatusCodeReasonPhrase { status_code: &408, reason_phrase: "Request Timeout" },
269    n409_conflict: &StatusCodeReasonPhrase { status_code: &409, reason_phrase: "Conflict" },
270    n410_gone: &StatusCodeReasonPhrase { status_code: &410, reason_phrase: "Gone" },
271    n411_length_required: &StatusCodeReasonPhrase { status_code: &411, reason_phrase: "Length Required" },
272    n412_precondition_failed: &StatusCodeReasonPhrase { status_code: &412, reason_phrase: "Precondition Failed" },
273    n413_payload_too_large: &StatusCodeReasonPhrase { status_code: &413, reason_phrase: "Payload Too Large" },
274    n414_uri_too_long: &StatusCodeReasonPhrase { status_code: &414, reason_phrase: "URI Too Long" },
275    n415_unsupported_media_type: &StatusCodeReasonPhrase { status_code: &415, reason_phrase: "Unsupported Media Type" },
276    n416_range_not_satisfiable: &StatusCodeReasonPhrase {
277        status_code: &416,
278        reason_phrase: "Range Not Satisfiable"
279    },
280
281    n417_expectation_failed: &StatusCodeReasonPhrase { status_code: &417, reason_phrase: "Expectation Failed" },
282    n418_im_a_teapot: &StatusCodeReasonPhrase { status_code: &418, reason_phrase: "I'm A Teapot" },
283    n421_misdirected_request: &StatusCodeReasonPhrase { status_code: &421, reason_phrase: "Misdirected Request" },
284    n422_unprocessable_entity: &StatusCodeReasonPhrase { status_code: &422, reason_phrase: "Unprocessable Entity" },
285    n423_locked: &StatusCodeReasonPhrase { status_code: &423, reason_phrase: "Locked" },
286    n424_failed_dependency: &StatusCodeReasonPhrase { status_code: &424, reason_phrase: "Failed Dependency" },
287    n425_too_early: &StatusCodeReasonPhrase { status_code: &425, reason_phrase: "Too Early" },
288    n426_upgrade_required: &StatusCodeReasonPhrase { status_code: &426, reason_phrase: "Upgrade Required" },
289    n428_precondition_required: &StatusCodeReasonPhrase { status_code: &428, reason_phrase: "Precondition Required" },
290    n429_too_many_requests: &StatusCodeReasonPhrase { status_code: &429, reason_phrase: "Too Many Requests" },
291    n431_request_header_fields_too_large: &StatusCodeReasonPhrase { status_code: &431, reason_phrase: "Request Header Fields Too Large" },
292    n451_unavailable_for_legal_reasons: &StatusCodeReasonPhrase { status_code: &451, reason_phrase: "Unavailable For Legal Reasons" },
293    n500_internal_server_error: &StatusCodeReasonPhrase {
294        status_code: &500,
295        reason_phrase: "Internal Server Error"
296    },
297    n501_not_implemented: &StatusCodeReasonPhrase { status_code: &501, reason_phrase: "Not Implemented" },
298    n502_bad_gateway: &StatusCodeReasonPhrase { status_code: &502, reason_phrase: "Bad Gateway" },
299    n503_service_unavailable: &StatusCodeReasonPhrase { status_code: &503, reason_phrase: "Service Unavailable" },
300    n504_gateway_timeout: &StatusCodeReasonPhrase { status_code: &504, reason_phrase: "Gateway Timeout" },
301    n505_http_version_not_supported: &StatusCodeReasonPhrase { status_code: &505, reason_phrase: "HTTP Version Not Supported" },
302    n506_variant_also_negotiates: &StatusCodeReasonPhrase { status_code: &506, reason_phrase: "Variant Also Negotiates" },
303    n507_insufficient_storage: &StatusCodeReasonPhrase { status_code: &507, reason_phrase: "Insufficient Storage" },
304    n508_loop_detected: &StatusCodeReasonPhrase { status_code: &508, reason_phrase: "Loop Detected" },
305    n510_not_extended: &StatusCodeReasonPhrase { status_code: &510, reason_phrase: "Not Extended" },
306    n511_network_authentication_required: &StatusCodeReasonPhrase { status_code: &511, reason_phrase: "Network Authentication Required" }
307};
308
309/// The `Content-Range` value for a single-part body, or `None` when the
310/// response shouldn't carry one. RFC 9110 §14.4 gives `Content-Range` meaning
311/// only on `206 Partial Content` (and `416`, which rws answers with a plain
312/// error body), so full `200` responses get no such header. `range.end` is
313/// inclusive.
314pub fn content_range_header_value(status_code: i16, content_range: &ContentRange) -> Option<String> {
315    if status_code != *STATUS_CODE_REASON_PHRASE.n206_partial_content.status_code {
316        return None;
317    }
318    Some(format!(
319        "{} {}-{}/{}",
320        Range::BYTES, content_range.range.start, content_range.range.end, content_range.size
321    ))
322}
323
324impl Response {
325
326    /// A response with nothing in it and no allocations — a placeholder to
327    /// `std::mem::replace` a real one with.
328    #[doc(hidden)]
329    pub fn empty() -> Response {
330        Response {
331            http_version: String::new(),
332            status_code: 0,
333            reason_phrase: String::new(),
334            headers: Vec::new(),
335            content_range_list: Vec::new(),
336            stream_file: None,
337            stream_pipe: None,
338            trailers: None,
339        }
340    }
341
342    /// A response with `status`, no headers and no body; set fields or push
343    /// headers and body parts after.
344    ///
345    /// ```rust
346    /// # extern crate rws_http as rust_web_server;
347    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
348    ///
349    /// let mut response = Response::with_status(STATUS_CODE_REASON_PHRASE.n204_no_content);
350    /// response.headers.push(rust_web_server::header::Header::new("X-Done", "yes"));
351    /// assert_eq!(response.status_code, 204);
352    /// ```
353    pub fn with_status(status: &StatusCodeReasonPhrase) -> Response {
354        let mut response = <Response as New>::new();
355        response.set_status(status);
356        response
357    }
358
359    /// Set the status code and reason phrase, reusing the phrase's buffer.
360    #[doc(hidden)]
361    pub fn set_status(&mut self, status: &StatusCodeReasonPhrase) {
362        self.status_code = *status.status_code;
363        self.reason_phrase.clear();
364        self.reason_phrase.push_str(status.reason_phrase);
365    }
366
367    pub fn build(status: StatusCodeReasonPhrase, header_list : Vec<Header>, body: Vec<ContentRange>) -> Response {
368        Response {
369            http_version: VERSION.http_1_1.to_string(),
370            status_code: *status.status_code,
371            reason_phrase: status.reason_phrase.to_string(),
372            headers: header_list,
373            content_range_list: body,
374            stream_file: None,
375            stream_pipe: None,
376            trailers: None,
377        }
378    }
379
380    pub fn status_code_reason_phrase_list() -> Vec<&'static StatusCodeReasonPhrase> {
381        let list = vec![
382            STATUS_CODE_REASON_PHRASE.n100_continue,
383            STATUS_CODE_REASON_PHRASE.n101_switching_protocols,
384            STATUS_CODE_REASON_PHRASE.n102_processing,
385            STATUS_CODE_REASON_PHRASE.n103_early_hints,
386            STATUS_CODE_REASON_PHRASE.n200_ok,
387            STATUS_CODE_REASON_PHRASE.n201_created,
388            STATUS_CODE_REASON_PHRASE.n202_accepted,
389            STATUS_CODE_REASON_PHRASE.n203_non_authoritative_information,
390            STATUS_CODE_REASON_PHRASE.n204_no_content,
391            STATUS_CODE_REASON_PHRASE.n205_reset_content,
392            STATUS_CODE_REASON_PHRASE.n206_partial_content,
393            STATUS_CODE_REASON_PHRASE.n207_multi_status,
394            STATUS_CODE_REASON_PHRASE.n208_already_reported,
395            STATUS_CODE_REASON_PHRASE.n226_im_used,
396            STATUS_CODE_REASON_PHRASE.n300_multiple_choices,
397            STATUS_CODE_REASON_PHRASE.n301_moved_permanently,
398            STATUS_CODE_REASON_PHRASE.n302_found,
399            STATUS_CODE_REASON_PHRASE.n303_see_other,
400            STATUS_CODE_REASON_PHRASE.n304_not_modified,
401            STATUS_CODE_REASON_PHRASE.n307_temporary_redirect,
402            STATUS_CODE_REASON_PHRASE.n308_permanent_redirect,
403            STATUS_CODE_REASON_PHRASE.n400_bad_request,
404            STATUS_CODE_REASON_PHRASE.n401_unauthorized,
405            STATUS_CODE_REASON_PHRASE.n402_payment_required,
406            STATUS_CODE_REASON_PHRASE.n403_forbidden,
407            STATUS_CODE_REASON_PHRASE.n404_not_found,
408            STATUS_CODE_REASON_PHRASE.n405_method_not_allowed,
409            STATUS_CODE_REASON_PHRASE.n406_not_acceptable,
410            STATUS_CODE_REASON_PHRASE.n407_proxy_authentication_required,
411            STATUS_CODE_REASON_PHRASE.n408_request_timeout,
412            STATUS_CODE_REASON_PHRASE.n409_conflict,
413            STATUS_CODE_REASON_PHRASE.n410_gone,
414            STATUS_CODE_REASON_PHRASE.n411_length_required,
415            STATUS_CODE_REASON_PHRASE.n412_precondition_failed,
416            STATUS_CODE_REASON_PHRASE.n413_payload_too_large,
417            STATUS_CODE_REASON_PHRASE.n414_uri_too_long,
418            STATUS_CODE_REASON_PHRASE.n415_unsupported_media_type,
419            STATUS_CODE_REASON_PHRASE.n416_range_not_satisfiable,
420            STATUS_CODE_REASON_PHRASE.n417_expectation_failed,
421            STATUS_CODE_REASON_PHRASE.n418_im_a_teapot,
422            STATUS_CODE_REASON_PHRASE.n421_misdirected_request,
423            STATUS_CODE_REASON_PHRASE.n422_unprocessable_entity,
424            STATUS_CODE_REASON_PHRASE.n423_locked,
425            STATUS_CODE_REASON_PHRASE.n424_failed_dependency,
426            STATUS_CODE_REASON_PHRASE.n425_too_early,
427            STATUS_CODE_REASON_PHRASE.n426_upgrade_required,
428            STATUS_CODE_REASON_PHRASE.n428_precondition_required,
429            STATUS_CODE_REASON_PHRASE.n429_too_many_requests,
430            STATUS_CODE_REASON_PHRASE.n431_request_header_fields_too_large,
431            STATUS_CODE_REASON_PHRASE.n451_unavailable_for_legal_reasons,
432            STATUS_CODE_REASON_PHRASE.n500_internal_server_error,
433            STATUS_CODE_REASON_PHRASE.n501_not_implemented,
434            STATUS_CODE_REASON_PHRASE.n502_bad_gateway,
435            STATUS_CODE_REASON_PHRASE.n503_service_unavailable,
436            STATUS_CODE_REASON_PHRASE.n504_gateway_timeout,
437            STATUS_CODE_REASON_PHRASE.n505_http_version_not_supported,
438            STATUS_CODE_REASON_PHRASE.n506_variant_also_negotiates,
439            STATUS_CODE_REASON_PHRASE.n507_insufficient_storage,
440            STATUS_CODE_REASON_PHRASE.n508_loop_detected,
441            STATUS_CODE_REASON_PHRASE.n510_not_extended,
442            STATUS_CODE_REASON_PHRASE.n511_network_authentication_required,
443        ];
444        list
445    }
446
447    pub const _ERROR_UNABLE_TO_PARSE_HTTP_VERSION_STATUS_CODE: &'static str = "Unable to parse status code";
448
449    pub const _HTTP_VERSION_AND_STATUS_CODE_AND_REASON_PHRASE_REGEX: &'static str = "(?P<http_version>\\w+/\\w+.\\w)\\s(?P<status_code>\\w+)\\s(?P<reason_phrase>.+)";
450
451    pub fn _get_header(&self, name: String) -> Option<&Header> {
452        // Header names are case-insensitive (RFC 9110 §5.1), as in Request::get_header.
453        let header =  self.headers.iter().find(|x| x.name.eq_ignore_ascii_case(&name));
454        header
455    }
456
457    /// `true` for a status that never has content: `1xx`, `204`, `304`
458    /// (RFC 9110 §6.4.1, §15.3.5, §15.4.5).
459    pub fn status_has_no_content(status_code: i16) -> bool {
460        (100..200).contains(&status_code) || status_code == 204 || status_code == 304
461    }
462
463    /// For a status without content ([`Response::status_has_no_content`]),
464    /// drop the body — buffered, file or pipe — and any `Content-Length` or
465    /// `Transfer-Encoding`, which those responses must not carry (#54).
466    /// Every connection writer calls it before framing the response.
467    pub fn drop_content_for_status(&mut self) {
468        if !Response::status_has_no_content(self.status_code) {
469            return;
470        }
471        self.content_range_list.clear();
472        self.stream_file = None;
473        self.stream_pipe = None;
474        self.headers.retain(|h| {
475            !h.name.eq_ignore_ascii_case(Header::_CONTENT_LENGTH)
476                && !h.name.eq_ignore_ascii_case(Header::_TRANSFER_ENCODING)
477        });
478    }
479
480    pub fn generate_body(content_range_list: Vec<ContentRange>) -> Vec<u8> {
481        let mut body = vec![];
482        let one = 1;
483
484        if content_range_list.len() == one {
485            // Move the body out rather than copy it: for a large file that
486            // copy was a second full-size buffer per in-flight response.
487            let mut content_range_list = content_range_list;
488            return content_range_list.swap_remove(0).body;
489        }
490
491        if content_range_list.len() > one {
492            // RFC 2046 §5.1.1: each part opens with "--boundary", the body
493            // closes with "--boundary--" (#53).
494            let boundary = Range::multipart_boundary(&content_range_list);
495            for (i, content_range) in content_range_list.iter().enumerate() {
496                let mut body_str = SYMBOL.empty_string.to_string();
497                if i != 0 {
498                    body_str.push_str(SYMBOL.new_line_carriage_return);
499                }
500                body_str.push_str(SYMBOL.hyphen);
501                body_str.push_str(SYMBOL.hyphen);
502                body_str.push_str(&boundary);
503                body_str.push_str(SYMBOL.new_line_carriage_return);
504                let content_type = [Header::_CONTENT_TYPE, Header::NAME_VALUE_SEPARATOR, SYMBOL.whitespace, &Header::single_line(&content_range.content_type)].join("");
505                body_str.push_str(content_type.as_str());
506                body_str.push_str(SYMBOL.new_line_carriage_return);
507                let content_range_header = [Header::_CONTENT_RANGE, Header::NAME_VALUE_SEPARATOR, SYMBOL.whitespace, Range::BYTES, SYMBOL.whitespace, &content_range.range.start.to_string(), SYMBOL.hyphen, &content_range.range.end.to_string(), SYMBOL.slash, &Header::single_line(&content_range.size)].join("");
508                body_str.push_str(content_range_header.as_str());
509                body_str.push_str(SYMBOL.new_line_carriage_return);
510                body_str.push_str(SYMBOL.new_line_carriage_return);
511
512                body.extend_from_slice(body_str.as_bytes());
513                body.extend_from_slice(&content_range.body);
514            }
515            body.extend_from_slice(b"\r\n--");
516            body.extend_from_slice(boundary.as_bytes());
517            body.extend_from_slice(b"--\r\n");
518        }
519
520        body
521    }
522
523    /// The serialized response: status line + headers, then the body.
524    pub fn generate_response(response: Response, request: Request) -> Vec<u8> {
525        Response::generate_response_coalesced(response, request, usize::MAX).0
526    }
527
528    /// Like [`generate_response`](Self::generate_response), but returns the
529    /// head (status line + headers + blank line) and the body separately, so
530    /// a connection writer can send them back to back without first copying
531    /// a large body into one combined buffer. The body is empty for HEAD and
532    /// for a status without content.
533    pub fn generate_response_parts(response: Response, request: Request) -> (Vec<u8>, Vec<u8>) {
534        Response::generate_response_coalesced(response, request, 0)
535    }
536
537    /// [`generate_response_parts`](Self::generate_response_parts), except
538    /// that a body of at most `coalesce_limit` bytes is appended to the head
539    /// (sized for both up front) and the returned body is then empty — one
540    /// buffer for one `write`, without growing the head afterwards.
541    #[doc(hidden)]
542    pub fn generate_response_coalesced(mut response: Response, request: Request, coalesce_limit: usize) -> (Vec<u8>, Vec<u8>) {
543        response.drop_content_for_status();
544        let streamed = response.stream_pipe.is_some() || response.stream_file.is_some();
545        let Response { http_version, status_code, reason_phrase, headers, mut content_range_list, .. } = response;
546
547        // Content-Type, Content-Range and Content-Length follow the other
548        // headers; they're written straight into the head rather than pushed
549        // onto `headers` first.
550        let mut content_type: Option<String> = None;
551        let mut content_range: Option<String> = None;
552        let mut content_length: Option<Decimal> = None;
553        if content_range_list.len() == 1 {
554            let range = &mut content_range_list[0];
555            content_range = content_range_header_value(status_code, range);
556            content_length = Some(Decimal::new(range.body.len() as u128));
557            // A single range's body is moved out by generate_body, which
558            // doesn't look at its type.
559            content_type = Some(std::mem::take(&mut range.content_type));
560        }
561        let multipart = content_range_list.len() > 1;
562        if multipart {
563            content_type = Some(Range::multipart_content_type(&content_range_list));
564        }
565
566        // HEAD: the head of the GET response, no body. OPTIONS gets its body
567        // like any other method: dropping it under a Content-Length that
568        // announced it left the client waiting (#54).
569        let is_head = request.method == METHOD.head;
570        let body = if is_head && !multipart {
571            Vec::new()
572        } else {
573            Response::generate_body(content_range_list)
574        };
575        if multipart {
576            // Without it a keep-alive client can't tell where the body ends
577            // (RFC 9112 §6.3; #53). A HEAD gets the GET's length.
578            content_length = Some(Decimal::new(body.len() as u128));
579        }
580        // No body at all: say so, or a keep-alive client waits for one until
581        // the connection closes (a gRPC-web trailers-only error hung a
582        // browser). Not for statuses that never have content, nor when the
583        // handler set the length itself, nor for a streamed body.
584        if content_length.is_none()
585            && !streamed
586            && !Response::status_has_no_content(status_code)
587            && !headers.iter().any(|h| h.name.eq_ignore_ascii_case(Header::_CONTENT_LENGTH))
588        {
589            content_length = Some(Decimal::new(0));
590        }
591        let body = if is_head { Vec::new() } else { body };
592        let coalesce = body.len() <= coalesce_limit;
593
594        // One buffer, sized up front, written once: this used to build a
595        // String per header, grow a second one, join the status line and
596        // copy it all again with format! — the biggest allocation cost of a
597        // small response.
598        let status = u16::try_from(status_code).map(|code| Decimal::new(code as u128));
599        let status_text = match &status {
600            Ok(decimal) => std::borrow::Cow::Borrowed(decimal.as_str()),
601            Err(_) => std::borrow::Cow::Owned(status_code.to_string()),
602        };
603        let extra = [
604            (Header::_CONTENT_TYPE, content_type.as_deref()),
605            (Header::_CONTENT_RANGE, content_range.as_deref()),
606            (Header::_CONTENT_LENGTH, content_length.as_ref().map(Decimal::as_str)),
607        ];
608        let capacity = http_version.len() + status_text.len() + reason_phrase.len() + 4
609            + headers.iter().map(|h| h.name.len() + h.value.len() + 4).sum::<usize>()
610            + extra.iter().map(|(name, value)| value.map_or(0, |v| name.len() + v.len() + 4)).sum::<usize>()
611            + 2
612            + if coalesce { body.len() } else { 0 };
613        let mut head = Vec::with_capacity(capacity);
614        head.extend_from_slice(http_version.as_bytes());
615        head.push(b' ');
616        head.extend_from_slice(status_text.as_bytes());
617        head.push(b' ');
618        head.extend_from_slice(Header::single_line(&reason_phrase).as_bytes());
619        head.extend_from_slice(b"\r\n");
620        let header_lines = headers.iter().map(|h| (h.name.as_ref(), h.value.as_ref()))
621            .chain(extra.iter().filter_map(|(name, value)| value.map(|v| (*name, v))));
622        for (name, value) in header_lines {
623            head.extend_from_slice(Header::single_line(name).as_bytes());
624            head.extend_from_slice(Header::NAME_VALUE_SEPARATOR.as_bytes());
625            head.extend_from_slice(Header::single_line(value).as_bytes());
626            head.extend_from_slice(b"\r\n");
627        }
628        head.extend_from_slice(b"\r\n");
629
630        if coalesce {
631            head.extend_from_slice(&body);
632            (head, Vec::new())
633        } else {
634            (head, body)
635        }
636    }
637
638    pub fn _parse_response(response_vec_u8: &[u8]) -> Response {
639        let mut cursor = io::Cursor::new(response_vec_u8);
640
641        let mut response = Response {
642            http_version: "".to_string(),
643            status_code: 0,
644            reason_phrase: "".to_string(),
645            headers: vec![],
646            content_range_list: vec![],
647            stream_file: None,
648            stream_pipe: None,
649            trailers: None,
650        };
651
652        let content_length: usize = 0;
653        let iteration_number : usize = 0;
654
655        Response::_parse_raw_response_via_cursor(&mut cursor, iteration_number, &mut response, content_length);
656
657        return response;
658    }
659
660    pub fn _parse_http_version_status_code_reason_phrase_string(http_version_status_code_reason_phrase: &str) -> Result<(String, i16, String), String> {
661        let line = without_cr_lf(http_version_status_code_reason_phrase);
662        let (http_version, status_code, reason_phrase) = Response::status_line_parts(&line)?;
663        Ok((http_version.to_string(), status_code, reason_phrase.to_string()))
664    }
665
666    /// `HTTP-version SP status-code [SP reason-phrase]`, CR/LF already
667    /// removed, borrowed from `line`.
668    fn status_line_parts(line: &str) -> Result<(&str, i16, &str), String> {
669        let boxed_split = line.split_once(SYMBOL.whitespace);
670        if boxed_split.is_none() {
671            return Err(Response::_ERROR_UNABLE_TO_PARSE_HTTP_VERSION_STATUS_CODE.to_string())
672        }
673
674        let (http_version, status_code_reason_phrase) = boxed_split.unwrap();
675        if !HTTP::is_supported_version(http_version) {
676            return Err(Response::_ERROR_UNABLE_TO_PARSE_HTTP_VERSION_STATUS_CODE.to_string())
677        }
678
679        // RFC 9112 §4: status-line = HTTP-version SP status-code SP [ reason-phrase ].
680        // The reason phrase is optional free text that clients should ignore
681        // (Python's http.server sends "404 File not found"), and any 3-digit
682        // code is valid — so neither is checked against the known list.
683        let (status_code, reason_phrase) = status_code_reason_phrase
684            .split_once(SYMBOL.whitespace)
685            .unwrap_or((status_code_reason_phrase, ""));
686
687        let is_three_digits = status_code.len() == 3 && status_code.bytes().all(|b| b.is_ascii_digit());
688        let status_code_i16 = match status_code.parse::<i16>() {
689            Ok(code) if is_three_digits && code >= 100 => code,
690            _ => return Err(Response::_ERROR_UNABLE_TO_PARSE_HTTP_VERSION_STATUS_CODE.to_string()),
691        };
692
693        Ok((http_version, status_code_i16, reason_phrase))
694    }
695
696    pub fn _parse_http_response_header_string(header_string: &str) -> Header {
697        // A line without ": " (malformed upstream input) is a name with an
698        // empty value, as in Request::parse_http_request_header_string —
699        // not an index-out-of-bounds panic.
700        let (header_name, raw_header_value) = header_string
701            .split_once(Header::NAME_VALUE_SEPARATOR)
702            .unwrap_or((header_string, ""));
703        let header_value = StringExt::truncate_new_line_carriage_return(raw_header_value);
704
705
706        Header {
707            name: header_name.to_string().into(),
708            value: header_value.to_string().into()
709        }
710    }
711
712    pub fn _parse_raw_response_via_cursor(
713        cursor: &mut Cursor<&[u8]>,
714        mut iteration_number: usize,
715        response: &mut Response,
716        mut content_length: usize) {
717
718        let mut buffer = vec![];
719        let boxed_read = cursor.read_until(b'\n', &mut buffer);
720        if boxed_read.is_err() {
721            eprintln!("unable to parse raw response via cursor {}", boxed_read.err().unwrap());
722            return;
723        }
724        let bytes_offset = boxed_read.unwrap();
725        let mut buffer_as_u8_array: &[u8] = &buffer;
726        let string = String::from_utf8(Vec::from(buffer_as_u8_array)).unwrap();
727
728        let is_first_iteration = iteration_number == 0;
729        let new_line_char_found = bytes_offset != 0;
730        let current_string_is_empty = string.trim().len() == 0;
731
732        if is_first_iteration {
733            let boxed_http_version_status_code_reason_phrase = Response::_parse_http_version_status_code_reason_phrase_string(&string);
734            if boxed_http_version_status_code_reason_phrase.is_err() {
735                let error = boxed_http_version_status_code_reason_phrase.err().unwrap();
736                eprintln!("{}", error);
737                return;
738            }
739
740            let (http_version, status_code, reason_phrase) = boxed_http_version_status_code_reason_phrase.unwrap();
741
742            response.http_version = http_version;
743            response.status_code = status_code;
744            response.reason_phrase = reason_phrase;
745        }
746
747        if current_string_is_empty {
748            // No Content-Type (a 204, a 304): an empty, non-multipart body.
749            let content_type = response._get_header(Header::_CONTENT_TYPE.to_string()).cloned()
750                .unwrap_or(Header { name: Header::_CONTENT_TYPE.into(), value: "".into() });
751            let is_multipart = Response::_is_multipart_byteranges_content_type(&content_type);
752
753            if is_multipart {
754                let content_range_list : Vec<ContentRange> = vec![];
755
756                let boundary = content_type.value.split(';')
757                    .filter_map(|param| param.trim().strip_prefix("boundary="))
758                    .next()
759                    .map(|b| b.trim_matches('"').to_string())
760                    .unwrap_or_else(|| Range::STRING_SEPARATOR.to_string());
761                let mut buf = vec![];
762                let _ = cursor.read_until(b'\n', &mut buf);
763                let boxed_value = Range::_parse_multipart_body_with_boundary(cursor, content_range_list, &boundary);
764                let mut range_list = vec![];
765                if boxed_value.is_ok() {
766                    range_list = boxed_value.unwrap();
767                }
768                response.content_range_list = range_list;
769            } else {
770                buffer = vec![];
771                let boxed_read = cursor.read_to_end(&mut buffer);
772                if boxed_read.is_ok() {
773                    buffer_as_u8_array = &buffer;
774
775                    let content_range = ContentRange {
776                        unit: Range::BYTES.to_string(),
777                        range: Range {
778                            start: 0,
779                            end: buffer_as_u8_array.len() as u64
780                        },
781                        size: buffer_as_u8_array.len().to_string(),
782                        body: Vec::from(buffer_as_u8_array),
783                        content_type: content_type.value.to_string()
784                    };
785                    response.content_range_list = vec![content_range];
786                } else {
787                    let reason = boxed_read.err().unwrap();
788                    eprintln!("error reading file: {}", reason.to_string())
789                }
790
791            }
792
793            return;
794        }
795
796        if new_line_char_found && !current_string_is_empty {
797            let mut header = Header { name: "".into(), value: "".into() };
798            if !is_first_iteration {
799                header = Response::_parse_http_response_header_string(&string);
800                if header.name.eq_ignore_ascii_case(Header::_CONTENT_LENGTH) {
801                    // No error channel here: treat an invalid value as absent.
802                    content_length = header.value.trim().parse().unwrap_or(0);
803                }
804            }
805
806            response.headers.push(header);
807            iteration_number += 1;
808            Response::_parse_raw_response_via_cursor(cursor, iteration_number, response, content_length);
809        }
810    }
811
812    pub fn _is_multipart_byteranges_content_type(content_type: &Header) -> bool {
813        // Asked of every parsed (e.g. every proxied) response: no join.
814        content_type.value.strip_prefix(Range::MULTIPART)
815            .and_then(|rest| rest.strip_prefix(SYMBOL.slash))
816            .is_some_and(|rest| rest.starts_with(Range::BYTERANGES))
817    }
818
819
820    pub fn get_response(
821        status_code_reason_phrase: &StatusCodeReasonPhrase,
822        boxed_header_list: Option<Vec<Header>>,
823        boxed_content_range_list: Option<Vec<ContentRange>>) -> Response {
824
825        let mut header_list: Vec<Header> = vec![];
826        if boxed_header_list.is_some() {
827            header_list = boxed_header_list.unwrap();
828        }
829
830        let mut content_range_list: Vec<ContentRange> = vec![];
831        if boxed_content_range_list.is_some() {
832            content_range_list = boxed_content_range_list.unwrap();
833        }
834
835        let response = Response {
836            http_version: VERSION.http_1_1.to_string(),
837            status_code: *status_code_reason_phrase.status_code,
838            reason_phrase: status_code_reason_phrase.reason_phrase.to_string(),
839            headers: header_list,
840            content_range_list,
841            stream_file: None,
842            stream_pipe: None,
843            trailers: None,
844        };
845
846        response
847    }
848
849    /// Build a `Content-Type: application/json` response from already-serialized bytes.
850    ///
851    /// Takes raw bytes rather than a `Serialize` value so it carries no dependency on the
852    /// `serde` feature — pair it with `serde_json::to_vec` (or hand-rolled JSON) to build `body`.
853    /// For a one-line serialize-and-respond helper on a typed value, see [`crate::json::Json`]
854    /// (`serde` feature).
855    ///
856    /// ```rust,no_run
857    /// # extern crate rws_http as rust_web_server;
858    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
859    ///
860    /// let response = Response::json(STATUS_CODE_REASON_PHRASE.n200_ok, br#"{"ok":true}"#.to_vec());
861    /// ```
862    pub fn json(status_code_reason_phrase: &StatusCodeReasonPhrase, body: Vec<u8>) -> Response {
863        let content_range = Range::get_content_range(body, MimeType::APPLICATION_JSON.to_string());
864        Response::get_response(status_code_reason_phrase, None, Some(vec![content_range]))
865    }
866
867    /// Build a `Content-Type: text/plain` response from a body string.
868    ///
869    /// ```rust,no_run
870    /// # extern crate rws_http as rust_web_server;
871    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
872    ///
873    /// let response = Response::text(STATUS_CODE_REASON_PHRASE.n400_bad_request, "invalid input");
874    /// ```
875    pub fn text(status_code_reason_phrase: &StatusCodeReasonPhrase, body: &str) -> Response {
876        let content_range = Range::get_content_range(body.as_bytes().to_vec(), MimeType::TEXT_PLAIN.to_string());
877        Response::get_response(status_code_reason_phrase, None, Some(vec![content_range]))
878    }
879
880    pub fn generate(&mut self) -> Vec<u8> {
881        let response = &mut self.clone();
882        response.drop_content_for_status();
883
884        if response.content_range_list.len() == 1 {
885            let content_range_index = 0;
886            let content_range = response.content_range_list.get(content_range_index).unwrap();
887            response.headers.push(Header {
888                name: Header::_CONTENT_TYPE.into(),
889                value: content_range.content_type.to_string().into()
890            });
891
892            if let Some(value) = content_range_header_value(response.status_code, content_range) {
893                response.headers.push(Header {
894                    name: Header::_CONTENT_RANGE.into(),
895                    value: value.into(),
896                });
897            }
898
899            response.headers.push(Header {
900                name: Header::_CONTENT_LENGTH.into(),
901                value: content_range.body.len().to_string().into()
902            });
903        }
904
905        if response.content_range_list.len() > 1 {
906            response.headers.push(Header {
907                name: Header::_CONTENT_TYPE.into(),
908                value: Range::multipart_content_type(&response.content_range_list).into(),
909            });
910        }
911
912        let response_clone = response.clone();
913        let body = Response::generate_body(response_clone.content_range_list);
914        if response.content_range_list.len() > 1 {
915            response.headers.push(Header {
916                name: Header::_CONTENT_LENGTH.into(),
917                value: body.len().to_string().into(),
918            });
919        }
920
921        let mut headers_str = SYMBOL.new_line_carriage_return.to_string();
922
923        let header_list = response.headers.clone();
924        for header in header_list {
925            let mut header_string = SYMBOL.empty_string.to_string();
926            header_string.push_str(&Header::single_line(&header.name));
927            header_string.push_str(Header::NAME_VALUE_SEPARATOR);
928            header_string.push_str(&Header::single_line(&header.value));
929            header_string.push_str(SYMBOL.new_line_carriage_return);
930            headers_str.push_str(&header_string);
931        }
932
933        let response_clone = response.clone();
934        let status = [response_clone.http_version, response.status_code.to_string(), Header::single_line(&response_clone.reason_phrase).into_owned()].join(SYMBOL.whitespace);
935        let response_without_body = format!(
936            "{}{}{}",
937            status,
938            headers_str,
939            SYMBOL.new_line_carriage_return,
940        );
941
942
943        [response_without_body.into_bytes(), body].concat()
944    }
945
946    pub fn get_header(&self, name: String) -> Option<&Header> {
947        self._get_header(name)
948    }
949
950    /// The first header named `name` (case-insensitive), without allocating.
951    pub fn header(&self, name: &str) -> Option<&Header> {
952        self.headers.iter().find(|h| h.name.eq_ignore_ascii_case(name))
953    }
954
955    pub fn parse(response_vec_u8: &[u8]) -> Result<Response, String> {
956        let total_bytes : i32 = response_vec_u8.len() as i32;
957        let bytes_read = 0;
958        let mut cursor = io::Cursor::new(response_vec_u8);
959
960        let mut response = Response {
961            http_version: "".to_string(),
962            status_code: 0,
963            reason_phrase: "".to_string(),
964            // Typical upstream heads have 6–16 headers: grown 1→2→4→8→16
965            // this was four allocations per proxied response.
966            headers: Vec::with_capacity(16),
967            content_range_list: vec![],
968            stream_file: None,
969            stream_pipe: None,
970            trailers: None,
971        };
972
973        let content_length: usize = 0;
974        let iteration_number : usize = 0;
975
976        let boxed_parse = Response::parse_raw_response_via_cursor(
977            &mut cursor, iteration_number,
978            &mut response,
979            content_length,
980            total_bytes,
981            bytes_read
982        );
983        if boxed_parse.is_err() {
984            let message = boxed_parse.err().unwrap();
985            return Err(message);
986        }
987
988        return Ok(response);
989    }
990
991    /// [`Response::parse`] and `proxy::normalize_upstream_response` in one
992    /// pass, for a buffered upstream response: the status line is re-emitted
993    /// as HTTP/1.1, headers named in `dropped` (compared ignoring ASCII case)
994    /// are skipped before their value is copied, and every `Content-Type`
995    /// moves onto the body (the first one is its type). A
996    /// `multipart/byteranges` response goes the general way. The result is
997    /// what the two-step path builds (`response_tests::parse_upstream_*`) —
998    /// minus a copy per dropped header, a second scan for `Content-Type`
999    /// and the `retain` that removed them.
1000    #[doc(hidden)]
1001    pub fn parse_upstream(input: &[u8], dropped: &[&str]) -> Result<Response, String> {
1002        let mut response = Response {
1003            http_version: crate::http::VERSION.http_1_1.to_string(),
1004            status_code: 0,
1005            reason_phrase: String::new(),
1006            headers: Vec::with_capacity(16),
1007            content_range_list: vec![],
1008            stream_file: None,
1009            stream_pipe: None,
1010            trailers: None,
1011        };
1012        let mut content_type: Option<&str> = None;
1013        let mut pos = 0usize;
1014        let mut first = true;
1015        loop {
1016            let rest = &input[pos.min(input.len())..];
1017            let line_len = memchr::memchr(b'\n', rest).map_or(rest.len(), |i| i + 1);
1018            pos += line_len;
1019            let line = std::str::from_utf8(&rest[..line_len]).map_err(|e| e.to_string())?;
1020            if first {
1021                let status_line = without_cr_lf(line);
1022                let (_, status_code, reason_phrase) = Response::status_line_parts(&status_line)?;
1023                response.status_code = status_code;
1024                response.reason_phrase = reason_phrase.to_string();
1025            }
1026            if is_blank(line) {
1027                if content_type.is_some_and(|ct| ct.starts_with("multipart/byteranges")) {
1028                    return Response::parse(input).map(normalized_like_proxy(dropped));
1029                }
1030                let body = input[pos.min(input.len())..].to_vec();
1031                response.content_range_list = vec![ContentRange {
1032                    unit: Range::BYTES.to_string(),
1033                    range: Range { start: 0, end: body.len() as u64 },
1034                    size: Decimal::new(body.len() as u128).as_str().to_string(),
1035                    content_type: content_type.unwrap_or(MimeType::APPLICATION_OCTET_STREAM).to_string(),
1036                    body,
1037                }];
1038                return Ok(response);
1039            }
1040            if line_len == 0 {
1041                return Err("unable to parse".to_string());
1042            }
1043            if !first {
1044                let (name, raw_value) = split_name_value(line).ok_or_else(|| format!("unable to parse header: {}", line))?;
1045                if name.eq_ignore_ascii_case(Header::_CONTENT_LENGTH) {
1046                    let value = without_cr_lf(raw_value);
1047                    let _: usize = value.trim().parse().map_err(|_| format!("invalid Content-Length: {:?}", value))?;
1048                }
1049                if name.eq_ignore_ascii_case(Header::_CONTENT_TYPE) {
1050                    if content_type.is_none() {
1051                        // Borrowed from `input`; an embedded CR/LF (rare)
1052                        // falls back to the general path to stay exact.
1053                        match without_cr_lf(raw_value) {
1054                            std::borrow::Cow::Borrowed(v) => content_type = Some(v),
1055                            std::borrow::Cow::Owned(_) => return Response::parse(input).map(normalized_like_proxy(dropped)),
1056                        }
1057                    }
1058                    continue;
1059                }
1060                if !dropped.iter().any(|d| name.eq_ignore_ascii_case(d)) {
1061                    response.headers.push(Header {
1062                        name: Header::name_from(name),
1063                        value: without_cr_lf(raw_value).into_owned().into(),
1064                    });
1065                }
1066            }
1067            first = false;
1068        }
1069    }
1070
1071    pub fn parse_raw_response_via_cursor(
1072        cursor: &mut Cursor<&[u8]>,
1073        mut iteration_number: usize,
1074        response: &mut Response,
1075        _content_length: usize,
1076        total_bytes: i32,
1077        mut bytes_read: i32) -> Result<(), String> {
1078        // One pass over the input: each line is a slice of it (this used to
1079        // copy every line into a Vec and then a String, and strip CR/LF with
1080        // two `str::replace` allocations per header), and only the values
1081        // kept are allocated. A loop, not recursion: one stack frame per
1082        // header line let a backend overflow the stack.
1083        let input: &[u8] = cursor.get_ref();
1084        loop {
1085            let start = (cursor.position() as usize).min(input.len());
1086            let rest = &input[start..];
1087            let line_len = memchr::memchr(b'\n', rest).map_or(rest.len(), |i| i + 1);
1088            cursor.set_position((start + line_len) as u64);
1089            bytes_read += line_len as i32;
1090            let line = std::str::from_utf8(&rest[..line_len]).map_err(|e| e.to_string())?;
1091
1092            let is_first_iteration = iteration_number == 0;
1093            let new_line_char_found = line_len != 0;
1094            let current_string_is_empty = is_blank(line);
1095
1096            if is_first_iteration {
1097                let status_line = without_cr_lf(line);
1098                let (http_version, status_code, reason_phrase) = Response::status_line_parts(&status_line)?;
1099                response.http_version = http_version.to_string();
1100                response.status_code = status_code;
1101                response.reason_phrase = reason_phrase.to_string();
1102            }
1103
1104            if current_string_is_empty {
1105                // if response does not contain Content-Type, it will be defaulted to APPLICATION_OCTET_STREAM
1106                let content_type_header = response.header(Header::_CONTENT_TYPE);
1107                let is_multipart = content_type_header.map_or(false, Response::_is_multipart_byteranges_content_type);
1108
1109                if is_multipart {
1110                    let content_type = response.header(Header::_CONTENT_TYPE).ok_or("Content-Type is missing")?;
1111                    let boundary = FormMultipartData::extract_boundary(content_type.value.as_ref())
1112                        .map_err(|_| "unable to extract boundary from Content-Type".to_string())?;
1113                    let is_opening_boundary_read = false;
1114                    response.content_range_list = Range::parse_multipart_body_with_boundary(
1115                        cursor,
1116                        vec![],
1117                        boundary,
1118                        total_bytes,
1119                        bytes_read,
1120                        is_opening_boundary_read)?;
1121                } else {
1122                    let content_type = content_type_header.map_or(MimeType::APPLICATION_OCTET_STREAM, |h| h.value.as_ref());
1123                    let start = (cursor.position() as usize).min(input.len());
1124                    let body = input[start..].to_vec();
1125                    cursor.set_position(input.len() as u64);
1126                    let content_range = ContentRange {
1127                        unit: Range::BYTES.to_string(),
1128                        range: Range {
1129                            start: 0,
1130                            end: body.len() as u64
1131                        },
1132                        size: Decimal::new(body.len() as u128).as_str().to_string(),
1133                        content_type: content_type.to_string(),
1134                        body,
1135                    };
1136                    response.content_range_list = vec![content_range];
1137                }
1138
1139                return Ok(());
1140            }
1141
1142            if new_line_char_found {
1143                if !is_first_iteration {
1144                    let header = Response::parse_http_response_header_string(line)?;
1145                    if header.name.eq_ignore_ascii_case(Header::_CONTENT_LENGTH) {
1146                        // An upstream-supplied value: reject garbage instead of panicking.
1147                        let _: usize = header.value.trim().parse()
1148                            .map_err(|_| format!("invalid Content-Length: {:?}", header.value))?;
1149                    }
1150                    response.headers.push(header);
1151                }
1152
1153                iteration_number += 1;
1154            } else {
1155                return Err("unable to parse".to_string());
1156            }
1157        }
1158    }
1159
1160    pub fn parse_http_response_header_string(header_string: &str) -> Result<Header, String> {
1161        match split_name_value(header_string) {
1162            Some((header_name, raw_header_value)) => Ok(Header {
1163                name: Header::name_from(header_name),
1164                value: without_cr_lf(raw_header_value).into_owned().into(),
1165            }),
1166            None => Err(format!("unable to parse header: {}", header_string)),
1167        }
1168    }
1169}
1170
1171impl Default for Response {
1172    /// The same as `Response::new()`: HTTP/1.1, no headers, no body, status
1173    /// `501 Not Implemented` until you set one.
1174    fn default() -> Self {
1175        <Response as New>::new()
1176    }
1177}
1178
1179impl New for Response {
1180    fn new() -> Self {
1181        Response {
1182            http_version: VERSION.http_1_1.to_string(),
1183            status_code: *STATUS_CODE_REASON_PHRASE.n501_not_implemented.status_code,
1184            reason_phrase: STATUS_CODE_REASON_PHRASE.n501_not_implemented.reason_phrase.to_string(),
1185            headers: vec![],
1186            content_range_list: vec![],
1187            stream_file: None,
1188            stream_pipe: None,
1189            trailers: None,
1190        }
1191    }
1192}
1193/// The general path of [`Response::parse_upstream`]: what
1194/// `proxy::normalize_upstream_response` does to a parsed response.
1195fn normalized_like_proxy<'a>(dropped: &'a [&'a str]) -> impl FnOnce(Response) -> Response + 'a {
1196    move |mut response: Response| {
1197        response.http_version.clear();
1198        response.http_version.push_str(crate::http::VERSION.http_1_1);
1199        let has_body = !response.content_range_list.is_empty();
1200        if let [range] = response.content_range_list.as_mut_slice() {
1201            if let Some(h) = response.headers.iter().find(|h| h.name.eq_ignore_ascii_case(Header::_CONTENT_TYPE)) {
1202                range.content_type.clear();
1203                range.content_type.push_str(&h.value);
1204            }
1205        }
1206        response.headers.retain(|h| {
1207            if h.name.eq_ignore_ascii_case(Header::_CONTENT_TYPE) {
1208                return !has_body;
1209            }
1210            !dropped.iter().any(|d| h.name.eq_ignore_ascii_case(d))
1211        });
1212        response
1213    }
1214}
1215
1216/// `s` with every CR and LF removed — borrowed when they only end the line.
1217/// `line.trim().is_empty()`, deciding on the first byte for the usual
1218/// header line instead of decoding chars from both ends.
1219fn is_blank(line: &str) -> bool {
1220    match line.bytes().find(|b| !b.is_ascii_whitespace()) {
1221        None => true,
1222        Some(b) if b.is_ascii() => false,
1223        Some(_) => line.trim().is_empty(),
1224    }
1225}
1226
1227fn without_cr_lf(s: &str) -> std::borrow::Cow<'_, str> {
1228    let trimmed = s.strip_suffix('\n').unwrap_or(s);
1229    let trimmed = trimmed.strip_suffix('\r').unwrap_or(trimmed);
1230    if memchr::memchr2(b'\r', b'\n', trimmed.as_bytes()).is_some() {
1231        std::borrow::Cow::Owned(StringExt::truncate_new_line_carriage_return(s))
1232    } else {
1233        std::borrow::Cow::Borrowed(trimmed)
1234    }
1235}
1236
1237/// `line` split at its first `": "` — what `split_once(": ")` returns,
1238/// without a substring searcher.
1239fn split_name_value(line: &str) -> Option<(&str, &str)> {
1240    let bytes = line.as_bytes();
1241    let mut from = 0;
1242    while let Some(offset) = memchr::memchr(b':', &bytes[from..]) {
1243        let colon = from + offset;
1244        if bytes.get(colon + 1) == Some(&b' ') {
1245            return Some((&line[..colon], &line[colon + 2..]));
1246        }
1247        from = colon + 1;
1248    }
1249    None
1250}
1251
1252/// Headers rws regenerates for a buffered body, plus hop-by-hop headers
1253/// (RFC 9110 §7.6.1) that must not be forwarded.
1254#[doc(hidden)]
1255pub const REGENERATED_OR_HOP_BY_HOP: &[&str] = &[
1256    "content-length", "transfer-encoding", "connection", "keep-alive",
1257    "proxy-connection", "te", "trailer", "upgrade",
1258];