Skip to main content

nano_get/
response.rs

1use std::fmt::{self, Display, Formatter};
2use std::io::{BufRead, BufReader, Read};
3use std::str;
4
5use crate::auth::{parse_authenticate_headers, Challenge};
6use crate::errors::NanoGetError;
7use crate::request::{Header, Method};
8
9const READ_CHUNK_SIZE: usize = 8 * 1024;
10const MAX_HEADER_COUNT: usize = 1024;
11const MAX_LINE_BYTES: usize = 16 * 1024;
12const SMALL_BODY_FAST_PATH_LIMIT: usize = 64 * 1024;
13
14/// HTTP protocol version reported by the server response line.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum HttpVersion {
17    /// `HTTP/1.0`
18    Http10,
19    /// `HTTP/1.1`
20    Http11,
21}
22
23impl Display for HttpVersion {
24    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
25        match self {
26            Self::Http10 => write!(f, "HTTP/1.0"),
27            Self::Http11 => write!(f, "HTTP/1.1"),
28        }
29    }
30}
31
32/// Parsed HTTP response data.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct Response {
35    /// HTTP version parsed from the status line.
36    pub version: HttpVersion,
37    /// Numeric status code, for example `200` or `404`.
38    pub status_code: u16,
39    /// Reason phrase from the status line, for example `OK`.
40    pub reason_phrase: String,
41    /// Response headers in wire order. Duplicate header names are preserved.
42    pub headers: Vec<Header>,
43    /// Chunked transfer trailers, when present.
44    pub trailers: Vec<Header>,
45    /// Raw response body bytes.
46    pub body: Vec<u8>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub(crate) struct ResponseHead {
51    pub version: HttpVersion,
52    pub status_code: u16,
53    pub reason_phrase: String,
54    pub headers: Vec<Header>,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub(crate) enum BodyKind {
59    None,
60    ContentLength,
61    Chunked,
62    CloseDelimited,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub(crate) struct ParsedResponse {
67    pub response: Response,
68    pub body_kind: BodyKind,
69    pub connection_close: bool,
70}
71
72impl Response {
73    /// Returns the first header value matching `name`, using ASCII case-insensitive lookup.
74    pub fn header(&self, name: &str) -> Option<&str> {
75        self.headers
76            .iter()
77            .find(|header| header.matches_name(name))
78            .map(Header::value)
79    }
80
81    /// Iterates over all header values matching `name`, preserving wire order and duplicates.
82    pub fn headers_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a Header> + 'a {
83        self.headers
84            .iter()
85            .filter(move |header| header.matches_name(name))
86    }
87
88    /// Returns the first trailer value matching `name`, using ASCII case-insensitive lookup.
89    pub fn trailer(&self, name: &str) -> Option<&str> {
90        self.trailers
91            .iter()
92            .find(|header| header.matches_name(name))
93            .map(Header::value)
94    }
95
96    /// Parses `WWW-Authenticate` challenges from the response.
97    pub fn www_authenticate_challenges(&self) -> Result<Vec<Challenge>, NanoGetError> {
98        parse_authenticate_headers(&self.headers, "www-authenticate")
99    }
100
101    /// Parses `Proxy-Authenticate` challenges from the response.
102    pub fn proxy_authenticate_challenges(&self) -> Result<Vec<Challenge>, NanoGetError> {
103        parse_authenticate_headers(&self.headers, "proxy-authenticate")
104    }
105
106    /// Decodes the body as UTF-8 without taking ownership.
107    pub fn body_text(&self) -> Result<&str, NanoGetError> {
108        Ok(str::from_utf8(&self.body)?)
109    }
110
111    /// Consumes the response and decodes the body as UTF-8.
112    pub fn into_body_text(self) -> Result<String, NanoGetError> {
113        String::from_utf8(self.body).map_err(|error| NanoGetError::InvalidUtf8(error.utf8_error()))
114    }
115
116    /// Returns `true` when status is in the `2xx` range.
117    pub fn is_success(&self) -> bool {
118        (200..=299).contains(&self.status_code)
119    }
120
121    /// Returns `true` when status is in the `3xx` range.
122    pub fn is_redirection(&self) -> bool {
123        (300..=399).contains(&self.status_code)
124    }
125
126    /// Returns `true` when status is in the `4xx` range.
127    pub fn is_client_error(&self) -> bool {
128        (400..=499).contains(&self.status_code)
129    }
130
131    /// Returns `true` when status is in the `5xx` range.
132    pub fn is_server_error(&self) -> bool {
133        (500..=599).contains(&self.status_code)
134    }
135}
136
137#[cfg(test)]
138pub(crate) fn read_response<R: Read>(
139    reader: &mut BufReader<R>,
140    method: Method,
141) -> Result<Response, NanoGetError> {
142    Ok(read_parsed_response(reader, method, true)?.response)
143}
144
145pub(crate) fn read_parsed_response<R: Read>(
146    reader: &mut BufReader<R>,
147    method: Method,
148    strict: bool,
149) -> Result<ParsedResponse, NanoGetError> {
150    loop {
151        let head = read_response_head(reader, strict)?;
152
153        if (100..=199).contains(&head.status_code) && head.status_code != 101 {
154            continue;
155        }
156
157        let body_kind = determine_body_kind(&head.headers, method, head.status_code, strict)?;
158        let (body, trailers) = match body_kind {
159            BodyKind::None => (Vec::new(), Vec::new()),
160            BodyKind::Chunked => read_chunked_body(reader, strict)?,
161            BodyKind::ContentLength => {
162                let content_length = content_length(&head.headers)?.unwrap_or(0);
163                read_content_length_body(reader, content_length)?
164            }
165            BodyKind::CloseDelimited => read_eof_body(reader)?,
166        };
167
168        let connection_close = should_close_connection(head.version, &head.headers, body_kind);
169
170        return Ok(ParsedResponse {
171            response: Response {
172                version: head.version,
173                status_code: head.status_code,
174                reason_phrase: head.reason_phrase,
175                headers: head.headers,
176                trailers,
177                body,
178            },
179            body_kind,
180            connection_close,
181        });
182    }
183}
184
185pub(crate) fn read_response_head<R: BufRead>(
186    reader: &mut R,
187    strict: bool,
188) -> Result<ResponseHead, NanoGetError> {
189    let (status_line, status_line_has_crlf) = read_line(reader).map_err(|error| match error {
190        NanoGetError::Io(io_error) if io_error.kind() == std::io::ErrorKind::UnexpectedEof => {
191            NanoGetError::IncompleteMessage("unexpected EOF while reading status line".to_string())
192        }
193        NanoGetError::Io(error) => NanoGetError::MalformedStatusLine(error.to_string()),
194        NanoGetError::MalformedHeader(line) => NanoGetError::MalformedStatusLine(line),
195        other => other,
196    })?;
197    if strict && !status_line_has_crlf {
198        return Err(NanoGetError::MalformedStatusLine(status_line));
199    }
200    let (version, status_code, reason_phrase) = parse_status_line(&status_line)?;
201    let headers = read_headers(reader, strict)?;
202    Ok(ResponseHead {
203        version,
204        status_code,
205        reason_phrase,
206        headers,
207    })
208}
209
210fn read_headers<R: BufRead>(reader: &mut R, strict: bool) -> Result<Vec<Header>, NanoGetError> {
211    let mut headers = Vec::new();
212
213    loop {
214        let (line, has_crlf) = read_line(reader).map_err(|error| match error {
215            NanoGetError::Io(io_error) if io_error.kind() == std::io::ErrorKind::UnexpectedEof => {
216                NanoGetError::IncompleteMessage(
217                    "unexpected EOF while reading header section".to_string(),
218                )
219            }
220            NanoGetError::Io(io_error) => NanoGetError::MalformedHeader(io_error.to_string()),
221            other => other,
222        })?;
223        if strict && !has_crlf {
224            return Err(NanoGetError::MalformedHeader(line));
225        }
226
227        if line.is_empty() {
228            return Ok(headers);
229        }
230
231        if line.starts_with(' ') || line.starts_with('\t') {
232            return Err(NanoGetError::MalformedHeader(line));
233        }
234
235        let (name, value) = line
236            .split_once(':')
237            .ok_or_else(|| NanoGetError::MalformedHeader(line.clone()))?;
238        if headers.len() >= MAX_HEADER_COUNT {
239            return Err(NanoGetError::MalformedHeader(
240                "too many response headers".to_string(),
241            ));
242        }
243        headers.push(Header::new(name.to_string(), value.trim().to_string())?);
244    }
245}
246
247fn read_line<R: BufRead>(reader: &mut R) -> Result<(String, bool), NanoGetError> {
248    let mut line = Vec::new();
249    let mut limited = reader.take((MAX_LINE_BYTES + 1) as u64);
250    let bytes_read = limited.read_until(b'\n', &mut line)?;
251    if bytes_read == 0 {
252        return Err(NanoGetError::Io(std::io::Error::new(
253            std::io::ErrorKind::UnexpectedEof,
254            "unexpected EOF",
255        )));
256    }
257    if line.len() > MAX_LINE_BYTES {
258        return Err(NanoGetError::MalformedHeader(
259            "line exceeds maximum length".to_string(),
260        ));
261    }
262
263    let mut has_crlf = false;
264    if line.ends_with(b"\r\n") {
265        has_crlf = true;
266        line.truncate(line.len() - 2);
267    } else if line.ends_with(b"\n") {
268        line.truncate(line.len() - 1);
269    }
270
271    let text = String::from_utf8(line)
272        .map_err(|error| NanoGetError::MalformedHeader(error.utf8_error().to_string()))?;
273    Ok((text, has_crlf))
274}
275
276fn parse_status_line(line: &str) -> Result<(HttpVersion, u16, String), NanoGetError> {
277    let mut parts = line.splitn(3, ' ');
278    let version = match parts.next() {
279        Some("HTTP/1.0") => HttpVersion::Http10,
280        Some("HTTP/1.1") => HttpVersion::Http11,
281        _ => return Err(NanoGetError::MalformedStatusLine(line.to_string())),
282    };
283
284    let status_code_token = parts
285        .next()
286        .ok_or_else(|| NanoGetError::MalformedStatusLine(line.to_string()))?;
287    if status_code_token.len() != 3 || !status_code_token.bytes().all(|byte| byte.is_ascii_digit())
288    {
289        return Err(NanoGetError::MalformedStatusLine(line.to_string()));
290    }
291
292    let status_code = status_code_token
293        .parse::<u16>()
294        .map_err(|_| NanoGetError::MalformedStatusLine(line.to_string()))?;
295
296    let reason_phrase = parts.next().unwrap_or("").to_string();
297    Ok((version, status_code, reason_phrase))
298}
299
300fn determine_body_kind(
301    headers: &[Header],
302    method: Method,
303    status_code: u16,
304    strict: bool,
305) -> Result<BodyKind, NanoGetError> {
306    if response_has_no_body(method, status_code) {
307        return Ok(BodyKind::None);
308    }
309
310    let has_content_length = content_length(headers)?.is_some();
311    if let Some(transfer_encoding) = transfer_encoding(headers)? {
312        if strict && has_content_length {
313            return Err(NanoGetError::AmbiguousResponseFraming(
314                "response contains both Transfer-Encoding and Content-Length".to_string(),
315            ));
316        }
317        if transfer_encoding.eq_ignore_ascii_case("chunked") {
318            return Ok(BodyKind::Chunked);
319        }
320
321        return Err(NanoGetError::UnsupportedTransferEncoding(transfer_encoding));
322    }
323
324    if has_content_length {
325        return Ok(BodyKind::ContentLength);
326    }
327
328    Ok(BodyKind::CloseDelimited)
329}
330
331fn response_has_no_body(method: Method, status_code: u16) -> bool {
332    method == Method::Head
333        || (100..=199).contains(&status_code)
334        || status_code == 204
335        || status_code == 304
336}
337
338fn transfer_encoding(headers: &[Header]) -> Result<Option<String>, NanoGetError> {
339    let values: Vec<&str> = headers
340        .iter()
341        .filter(|header| header.matches_name("transfer-encoding"))
342        .map(Header::value)
343        .collect();
344
345    if values.is_empty() {
346        return Ok(None);
347    }
348
349    let tokens: Vec<String> = values
350        .iter()
351        .flat_map(|value| value.split(','))
352        .map(str::trim)
353        .filter(|value| !value.is_empty())
354        .map(str::to_string)
355        .collect();
356
357    if tokens.len() == 1 {
358        return Ok(Some(tokens[0].clone()));
359    }
360
361    Err(NanoGetError::UnsupportedTransferEncoding(tokens.join(",")))
362}
363
364pub(crate) fn content_length(headers: &[Header]) -> Result<Option<usize>, NanoGetError> {
365    let mut values = headers
366        .iter()
367        .filter(|header| header.matches_name("content-length"))
368        .flat_map(|header| header.value().split(','))
369        .map(str::trim)
370        .filter(|value| !value.is_empty())
371        .map(parse_content_length_value);
372
373    let Some(first) = values.next() else {
374        return Ok(None);
375    };
376    let first = first?;
377
378    for value in values {
379        if value? != first {
380            return Err(NanoGetError::InvalidContentLength(
381                "mismatched duplicate content-length headers".to_string(),
382            ));
383        }
384    }
385
386    Ok(Some(first))
387}
388
389fn parse_content_length_value(value: &str) -> Result<usize, NanoGetError> {
390    if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
391        return Err(NanoGetError::InvalidContentLength(value.to_string()));
392    }
393
394    value
395        .parse::<usize>()
396        .map_err(|_| NanoGetError::InvalidContentLength(value.to_string()))
397}
398
399fn read_content_length_body<R: Read>(
400    reader: &mut R,
401    content_length: usize,
402) -> Result<(Vec<u8>, Vec<Header>), NanoGetError> {
403    if content_length <= SMALL_BODY_FAST_PATH_LIMIT {
404        let mut body = vec![0u8; content_length];
405        reader.read_exact(&mut body).map_err(|error| {
406            if error.kind() == std::io::ErrorKind::UnexpectedEof {
407                NanoGetError::IncompleteMessage(
408                    "unexpected EOF while reading Content-Length body".to_string(),
409                )
410            } else {
411                NanoGetError::Io(error)
412            }
413        })?;
414        return Ok((body, Vec::new()));
415    }
416
417    let mut body = Vec::with_capacity(SMALL_BODY_FAST_PATH_LIMIT);
418    read_exact_into_vec(
419        reader,
420        &mut body,
421        content_length,
422        "unexpected EOF while reading Content-Length body",
423    )?;
424    Ok((body, Vec::new()))
425}
426
427fn read_eof_body<R: Read>(reader: &mut R) -> Result<(Vec<u8>, Vec<Header>), NanoGetError> {
428    let mut body = Vec::new();
429    reader.read_to_end(&mut body)?;
430    Ok((body, Vec::new()))
431}
432
433fn read_chunked_body<R: BufRead>(
434    reader: &mut R,
435    strict: bool,
436) -> Result<(Vec<u8>, Vec<Header>), NanoGetError> {
437    let mut body = Vec::new();
438
439    loop {
440        let (line, has_crlf) = read_line(reader).map_err(|error| match error {
441            NanoGetError::Io(io_error) if io_error.kind() == std::io::ErrorKind::UnexpectedEof => {
442                NanoGetError::IncompleteMessage(
443                    "unexpected EOF while reading chunk size".to_string(),
444                )
445            }
446            NanoGetError::Io(io_error) => NanoGetError::InvalidChunk(io_error.to_string()),
447            other => other,
448        })?;
449        if strict && !has_crlf {
450            return Err(NanoGetError::InvalidChunk(
451                "chunk-size line is not CRLF-terminated".to_string(),
452            ));
453        }
454        let size_token = line.split(';').next().unwrap_or("").trim();
455        let chunk_size = usize::from_str_radix(size_token, 16)
456            .map_err(|_| NanoGetError::InvalidChunk(line.clone()))?;
457
458        if chunk_size == 0 {
459            let trailers = read_headers(reader, strict)?;
460            return Ok((body, trailers));
461        }
462
463        if body.len().checked_add(chunk_size).is_none() {
464            return Err(NanoGetError::InvalidChunk(
465                "chunk-size overflow while assembling body".to_string(),
466            ));
467        }
468        if chunk_size <= SMALL_BODY_FAST_PATH_LIMIT {
469            let start = body.len();
470            body.resize(start + chunk_size, 0);
471            if let Err(error) = reader.read_exact(&mut body[start..]) {
472                body.truncate(start);
473                if error.kind() == std::io::ErrorKind::UnexpectedEof {
474                    return Err(NanoGetError::IncompleteMessage(
475                        "unexpected EOF while reading chunk body".to_string(),
476                    ));
477                }
478                return Err(NanoGetError::Io(error));
479            }
480        } else {
481            read_exact_into_vec(
482                reader,
483                &mut body,
484                chunk_size,
485                "unexpected EOF while reading chunk body",
486            )?;
487        }
488
489        let mut crlf = [0u8; 2];
490        reader.read_exact(&mut crlf).map_err(|error| {
491            if error.kind() == std::io::ErrorKind::UnexpectedEof {
492                NanoGetError::IncompleteMessage("unexpected EOF after chunk body".to_string())
493            } else {
494                NanoGetError::Io(error)
495            }
496        })?;
497        if crlf != *b"\r\n" {
498            return Err(NanoGetError::InvalidChunk(
499                "missing CRLF after chunk body".to_string(),
500            ));
501        }
502    }
503}
504
505fn read_exact_into_vec<R: Read>(
506    reader: &mut R,
507    body: &mut Vec<u8>,
508    mut remaining: usize,
509    eof_message: &str,
510) -> Result<(), NanoGetError> {
511    let mut chunk = [0u8; READ_CHUNK_SIZE];
512    while remaining > 0 {
513        let to_read = remaining.min(chunk.len());
514        match reader.read(&mut chunk[..to_read]) {
515            Ok(0) => {
516                return Err(NanoGetError::IncompleteMessage(eof_message.to_string()));
517            }
518            Ok(read) => {
519                body.extend_from_slice(&chunk[..read]);
520                remaining -= read;
521            }
522            Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
523                return Err(NanoGetError::IncompleteMessage(eof_message.to_string()));
524            }
525            Err(error) => return Err(NanoGetError::Io(error)),
526        }
527    }
528    Ok(())
529}
530
531pub(crate) fn should_close_connection(
532    version: HttpVersion,
533    headers: &[Header],
534    body_kind: BodyKind,
535) -> bool {
536    if body_kind == BodyKind::CloseDelimited {
537        return true;
538    }
539
540    if has_connection_token(headers, "close") {
541        return true;
542    }
543
544    version == HttpVersion::Http10 && !has_connection_token(headers, "keep-alive")
545}
546
547fn has_connection_token(headers: &[Header], token: &str) -> bool {
548    headers
549        .iter()
550        .filter(|header| header.matches_name("connection"))
551        .flat_map(|header| header.value().split(','))
552        .map(str::trim)
553        .any(|candidate| candidate.eq_ignore_ascii_case(token))
554}
555
556#[cfg(test)]
557pub(crate) fn parse_response_bytes(bytes: &[u8], method: Method) -> Result<Response, NanoGetError> {
558    let mut reader = BufReader::new(bytes);
559    read_response(&mut reader, method)
560}
561
562#[cfg(test)]
563mod tests {
564    use std::io::{self, BufRead, BufReader, Cursor, Read};
565    use std::panic::{self, AssertUnwindSafe};
566
567    use super::{
568        parse_response_bytes, read_chunked_body, read_content_length_body, read_parsed_response,
569        read_response_head, BodyKind, HttpVersion,
570    };
571    use crate::errors::NanoGetError;
572    use crate::request::Method;
573
574    #[test]
575    fn parses_content_length_response() {
576        let response = parse_response_bytes(
577            b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nX-Test: 1\r\n\r\nhello",
578            Method::Get,
579        )
580        .unwrap();
581        assert_eq!(response.version, HttpVersion::Http11);
582        assert_eq!(response.status_code, 200);
583        assert_eq!(response.reason_phrase, "OK");
584        assert_eq!(response.header("x-test"), Some("1"));
585        assert_eq!(response.body, b"hello");
586    }
587
588    #[test]
589    fn parses_reason_phrases_with_spaces() {
590        let response = parse_response_bytes(
591            b"HTTP/1.0 404 Not Found Here\r\nContent-Length: 0\r\n\r\n",
592            Method::Get,
593        )
594        .unwrap();
595        assert_eq!(response.version, HttpVersion::Http10);
596        assert_eq!(response.reason_phrase, "Not Found Here");
597    }
598
599    #[test]
600    fn parses_chunked_responses_and_trailers() {
601        let response = parse_response_bytes(
602            b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nrust\r\n6\r\nacean!\r\n0\r\nX-Trailer: done\r\n\r\n",
603            Method::Get,
604        )
605        .unwrap();
606        assert_eq!(response.body, b"rustacean!");
607        assert_eq!(response.trailer("x-trailer"), Some("done"));
608    }
609
610    #[test]
611    fn head_responses_ignore_declared_body() {
612        let response = parse_response_bytes(
613            b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
614            Method::Head,
615        )
616        .unwrap();
617        assert!(response.body.is_empty());
618    }
619
620    #[test]
621    fn parses_connection_close_bodies() {
622        let mut reader = BufReader::new(&b"HTTP/1.1 200 OK\r\n\r\neof body"[..]);
623        let parsed = read_parsed_response(&mut reader, Method::Get, true).unwrap();
624        assert_eq!(parsed.body_kind, BodyKind::CloseDelimited);
625        assert!(parsed.connection_close);
626        assert_eq!(parsed.response.body, b"eof body");
627    }
628
629    #[test]
630    fn rejects_invalid_status_lines() {
631        let error = parse_response_bytes(b"HTP/1.1 200 OK\r\n\r\n", Method::Get).unwrap_err();
632        assert!(matches!(error, NanoGetError::MalformedStatusLine(_)));
633
634        let error = parse_response_bytes(b"HTTP/1.1 20 OK\r\n\r\n", Method::Get).unwrap_err();
635        assert!(matches!(error, NanoGetError::MalformedStatusLine(_)));
636
637        let error = parse_response_bytes(b"HTTP/1.1 2000 OK\r\n\r\n", Method::Get).unwrap_err();
638        assert!(matches!(error, NanoGetError::MalformedStatusLine(_)));
639    }
640
641    #[test]
642    fn rejects_unsupported_transfer_encodings() {
643        let error = parse_response_bytes(
644            b"HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip\r\n\r\n",
645            Method::Get,
646        )
647        .unwrap_err();
648        assert!(matches!(
649            error,
650            NanoGetError::UnsupportedTransferEncoding(_)
651        ));
652
653        let error = parse_response_bytes(
654            b"HTTP/1.1 200 OK\r\nTransfer-Encoding: gzip, chunked\r\n\r\n",
655            Method::Get,
656        )
657        .unwrap_err();
658        assert!(matches!(
659            error,
660            NanoGetError::UnsupportedTransferEncoding(_)
661        ));
662    }
663
664    #[test]
665    fn rejects_mismatched_duplicate_content_lengths() {
666        let error = parse_response_bytes(
667            b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nContent-Length: 6\r\n\r\nhello!",
668            Method::Get,
669        )
670        .unwrap_err();
671        assert!(matches!(error, NanoGetError::InvalidContentLength(_)));
672    }
673
674    #[test]
675    fn accepts_duplicate_content_lengths_with_equal_numeric_values() {
676        let response = parse_response_bytes(
677            b"HTTP/1.1 200 OK\r\nContent-Length: 05\r\nContent-Length: 5\r\n\r\nhello",
678            Method::Get,
679        )
680        .unwrap();
681        assert_eq!(response.body, b"hello");
682    }
683
684    #[test]
685    fn rejects_non_numeric_content_lengths() {
686        let error = parse_response_bytes(
687            b"HTTP/1.1 200 OK\r\nContent-Length: +5\r\n\r\nhello",
688            Method::Get,
689        )
690        .unwrap_err();
691        assert!(matches!(error, NanoGetError::InvalidContentLength(_)));
692    }
693
694    #[test]
695    fn rejects_invalid_chunk_sizes() {
696        let error = parse_response_bytes(
697            b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nbogus\r\n",
698            Method::Get,
699        )
700        .unwrap_err();
701        assert!(matches!(error, NanoGetError::InvalidChunk(_)));
702    }
703
704    #[test]
705    fn body_text_reports_invalid_utf8() {
706        let response = parse_response_bytes(
707            b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n\xff\xff",
708            Method::Get,
709        )
710        .unwrap();
711        assert!(matches!(
712            response.body_text(),
713            Err(NanoGetError::InvalidUtf8(_))
714        ));
715    }
716
717    #[test]
718    fn skips_interim_responses_but_not_switching_protocols() {
719        let response = parse_response_bytes(
720            b"HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok",
721            Method::Get,
722        )
723        .unwrap();
724        assert_eq!(response.status_code, 200);
725        assert_eq!(response.body, b"ok");
726
727        let response =
728            parse_response_bytes(b"HTTP/1.1 101 Switching Protocols\r\n\r\n", Method::Get).unwrap();
729        assert_eq!(response.status_code, 101);
730    }
731
732    #[test]
733    fn rejects_malformed_headers() {
734        let error = parse_response_bytes(b"HTTP/1.1 200 OK\r\nBroken-Header\r\n\r\n", Method::Get)
735            .unwrap_err();
736        assert!(matches!(error, NanoGetError::MalformedHeader(_)));
737    }
738
739    #[test]
740    fn preserves_duplicate_headers() {
741        let response = parse_response_bytes(
742            b"HTTP/1.1 200 OK\r\nSet-Cookie: a=1\r\nSet-Cookie: b=2\r\nContent-Length: 0\r\n\r\n",
743            Method::Get,
744        )
745        .unwrap();
746        let cookies: Vec<_> = response
747            .headers_named("set-cookie")
748            .map(|header| header.value().to_string())
749            .collect();
750        assert_eq!(cookies, vec!["a=1".to_string(), "b=2".to_string()]);
751    }
752
753    #[test]
754    fn parses_response_heads() {
755        let mut reader = BufReader::new(&b"HTTP/1.1 200 OK\r\nX-Test: yes\r\n\r\n"[..]);
756        let head = read_response_head(&mut reader, true).unwrap();
757        assert_eq!(head.status_code, 200);
758        assert_eq!(head.headers[0].value(), "yes");
759    }
760
761    #[test]
762    fn keep_alive_is_honored_for_http_10() {
763        let mut reader = BufReader::new(
764            &b"HTTP/1.0 200 OK\r\nConnection: keep-alive\r\nContent-Length: 2\r\n\r\nok"[..],
765        );
766        let parsed = read_parsed_response(&mut reader, Method::Get, true).unwrap();
767        assert!(!parsed.connection_close);
768    }
769
770    #[test]
771    fn strict_mode_rejects_lf_only_lines() {
772        let mut reader = BufReader::new(&b"HTTP/1.1 200 OK\nContent-Length: 0\n\n"[..]);
773        let error = read_parsed_response(&mut reader, Method::Get, true).unwrap_err();
774        assert!(matches!(error, NanoGetError::MalformedStatusLine(_)));
775    }
776
777    #[test]
778    fn lenient_mode_accepts_lf_only_lines() {
779        let mut reader = BufReader::new(&b"HTTP/1.1 200 OK\nContent-Length: 2\n\nok"[..]);
780        let parsed = read_parsed_response(&mut reader, Method::Get, false).unwrap();
781        assert_eq!(parsed.response.body, b"ok");
782    }
783
784    #[test]
785    fn strict_mode_rejects_transfer_encoding_with_content_length() {
786        let mut reader = BufReader::new(
787            &b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nContent-Length: 2\r\n\r\n2\r\nok\r\n0\r\n\r\n"[..],
788        );
789        let error = read_parsed_response(&mut reader, Method::Get, true).unwrap_err();
790        assert!(matches!(error, NanoGetError::AmbiguousResponseFraming(_)));
791    }
792
793    #[test]
794    fn incomplete_content_length_body_reports_incomplete_message() {
795        let mut reader = BufReader::new(&b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\nok"[..]);
796        let error = read_parsed_response(&mut reader, Method::Get, true).unwrap_err();
797        assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
798    }
799
800    #[test]
801    fn incomplete_chunked_body_reports_incomplete_message() {
802        let mut reader =
803            BufReader::new(&b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\nok"[..]);
804        let error = read_parsed_response(&mut reader, Method::Get, true).unwrap_err();
805        assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
806    }
807
808    #[test]
809    fn response_status_helpers_cover_all_classes() {
810        let ok = parse_response_bytes(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", Method::Get)
811            .unwrap();
812        assert!(ok.is_success());
813        assert!(!ok.is_redirection());
814
815        let redirect = parse_response_bytes(
816            b"HTTP/1.1 302 Found\r\nContent-Length: 0\r\n\r\n",
817            Method::Get,
818        )
819        .unwrap();
820        assert!(redirect.is_redirection());
821
822        let client_error = parse_response_bytes(
823            b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n",
824            Method::Get,
825        )
826        .unwrap();
827        assert!(client_error.is_client_error());
828
829        let server_error = parse_response_bytes(
830            b"HTTP/1.1 500 Boom\r\nContent-Length: 0\r\n\r\n",
831            Method::Get,
832        )
833        .unwrap();
834        assert!(server_error.is_server_error());
835    }
836
837    #[test]
838    fn parses_authenticate_challenges_from_response_helpers() {
839        let response = parse_response_bytes(
840            b"HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Basic realm=\"api\"\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\nContent-Length: 0\r\n\r\n",
841            Method::Get,
842        )
843        .unwrap();
844        assert_eq!(response.www_authenticate_challenges().unwrap().len(), 1);
845        assert_eq!(response.proxy_authenticate_challenges().unwrap().len(), 1);
846    }
847
848    #[test]
849    fn http_version_display_formats_wire_tokens() {
850        assert_eq!(HttpVersion::Http10.to_string(), "HTTP/1.0");
851        assert_eq!(HttpVersion::Http11.to_string(), "HTTP/1.1");
852    }
853
854    #[test]
855    fn response_head_maps_status_and_header_read_io_errors() {
856        let mut empty = BufReader::new(&b""[..]);
857        let error = read_response_head(&mut empty, true).unwrap_err();
858        assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
859
860        let mut truncated = BufReader::new(&b"HTTP/1.1 200 OK\r\nX-Test: 1\r\n"[..]);
861        let error = read_response_head(&mut truncated, true).unwrap_err();
862        assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
863
864        let mut invalid_status = BufReader::new(&b"\xff\r\n\r\n"[..]);
865        let error = read_response_head(&mut invalid_status, true).unwrap_err();
866        assert!(matches!(error, NanoGetError::MalformedStatusLine(_)));
867
868        let mut invalid_header = BufReader::new(&b"HTTP/1.1 200 OK\r\nX:\xff\r\n\r\n"[..]);
869        let error = read_response_head(&mut invalid_header, true).unwrap_err();
870        assert!(matches!(error, NanoGetError::MalformedHeader(_)));
871    }
872
873    #[test]
874    fn strict_header_parsing_rejects_lf_only_and_obs_fold() {
875        let mut lf_only = BufReader::new(&b"HTTP/1.1 200 OK\r\nX-Test: 1\n\n"[..]);
876        let error = read_response_head(&mut lf_only, true).unwrap_err();
877        assert!(matches!(error, NanoGetError::MalformedHeader(_)));
878
879        let mut obs_fold = BufReader::new(&b"HTTP/1.1 200 OK\r\n value\r\n\r\n"[..]);
880        let error = read_response_head(&mut obs_fold, true).unwrap_err();
881        assert!(matches!(error, NanoGetError::MalformedHeader(_)));
882    }
883
884    struct FailingRead {
885        cursor: Cursor<Vec<u8>>,
886        fail_at: usize,
887        kind: io::ErrorKind,
888    }
889
890    impl Read for FailingRead {
891        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
892            let pos = self.cursor.position() as usize;
893            if pos >= self.fail_at {
894                return Err(io::Error::new(self.kind, "forced read failure"));
895            }
896            let max = (self.fail_at - pos).min(buf.len());
897            self.cursor.read(&mut buf[..max])
898        }
899    }
900
901    struct AlwaysErrBufRead;
902
903    impl Read for AlwaysErrBufRead {
904        fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
905            Err(io::Error::new(io::ErrorKind::Other, "forced read failure"))
906        }
907    }
908
909    impl BufRead for AlwaysErrBufRead {
910        fn fill_buf(&mut self) -> io::Result<&[u8]> {
911            Err(io::Error::new(io::ErrorKind::Other, "forced fill failure"))
912        }
913
914        fn consume(&mut self, _amt: usize) {}
915    }
916
917    struct DeterministicRng {
918        state: u64,
919    }
920
921    impl DeterministicRng {
922        fn new(seed: u64) -> Self {
923            Self { state: seed }
924        }
925
926        fn next_u32(&mut self) -> u32 {
927            self.state = self
928                .state
929                .wrapping_mul(6_364_136_223_846_793_005)
930                .wrapping_add(1);
931            (self.state >> 32) as u32
932        }
933
934        fn next_bytes(&mut self, max_len: usize) -> Vec<u8> {
935            let len = (self.next_u32() as usize) % (max_len + 1);
936            let mut bytes = Vec::with_capacity(len);
937            for _ in 0..len {
938                bytes.push((self.next_u32() & 0xff) as u8);
939            }
940            bytes
941        }
942    }
943
944    #[test]
945    fn body_readers_map_non_eof_io_failures() {
946        let mut failing = FailingRead {
947            cursor: Cursor::new(vec![1, 2, 3]),
948            fail_at: 0,
949            kind: io::ErrorKind::Other,
950        };
951        let error = read_content_length_body(&mut failing, 1).unwrap_err();
952        assert!(matches!(error, NanoGetError::Io(_)));
953
954        let mut failing_chunk = AlwaysErrBufRead;
955        let error = read_chunked_body(&mut failing_chunk, true).unwrap_err();
956        assert!(matches!(error, NanoGetError::InvalidChunk(_)));
957
958        let mut read_buf = [0u8; 1];
959        let mut always = AlwaysErrBufRead;
960        assert!(always.read(&mut read_buf).is_err());
961        always.consume(0);
962    }
963
964    #[test]
965    fn chunked_parser_covers_additional_error_paths() {
966        let mut empty = BufReader::new(&b""[..]);
967        let error = read_chunked_body(&mut empty, true).unwrap_err();
968        assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
969
970        let mut lf_only = BufReader::new(&b"2\nok\r\n0\r\n\r\n"[..]);
971        let error = read_chunked_body(&mut lf_only, true).unwrap_err();
972        assert!(matches!(error, NanoGetError::InvalidChunk(_)));
973
974        let mut body_fail = BufReader::new(FailingRead {
975            cursor: Cursor::new(b"2\r\nok\r\n0\r\n\r\n".to_vec()),
976            fail_at: 3,
977            kind: io::ErrorKind::Other,
978        });
979        let error = read_chunked_body(&mut body_fail, true).unwrap_err();
980        assert!(matches!(error, NanoGetError::Io(_)));
981
982        let mut crlf_eof = BufReader::new(&b"2\r\nok"[..]);
983        let error = read_chunked_body(&mut crlf_eof, true).unwrap_err();
984        assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
985
986        let mut crlf_fail = BufReader::new(FailingRead {
987            cursor: Cursor::new(b"2\r\nok\r\n0\r\n\r\n".to_vec()),
988            fail_at: 5,
989            kind: io::ErrorKind::Other,
990        });
991        let error = read_chunked_body(&mut crlf_fail, true).unwrap_err();
992        assert!(matches!(error, NanoGetError::Io(_)));
993
994        let mut bad_crlf = BufReader::new(&b"2\r\nokxx"[..]);
995        let error = read_chunked_body(&mut bad_crlf, true).unwrap_err();
996        assert!(matches!(error, NanoGetError::InvalidChunk(_)));
997
998        let mut invalid_utf8_chunk = BufReader::new(&b"\xff\n"[..]);
999        let error = read_chunked_body(&mut invalid_utf8_chunk, false).unwrap_err();
1000        assert!(matches!(error, NanoGetError::MalformedHeader(_)));
1001    }
1002
1003    #[test]
1004    fn rejects_excessive_header_count() {
1005        let mut wire = String::from("HTTP/1.1 200 OK\r\n");
1006        for index in 0..(super::MAX_HEADER_COUNT + 1) {
1007            wire.push_str(&format!("X-{index}: v\r\n"));
1008        }
1009        wire.push_str("Content-Length: 0\r\n\r\n");
1010        let error = parse_response_bytes(wire.as_bytes(), Method::Get).unwrap_err();
1011        assert!(matches!(error, NanoGetError::MalformedHeader(_)));
1012    }
1013
1014    #[test]
1015    fn rejects_overly_long_lines() {
1016        let long_value = "a".repeat(super::MAX_LINE_BYTES + 1);
1017        let wire = format!("HTTP/1.1 200 OK\r\nX-Test: {long_value}\r\n\r\n");
1018        let error = parse_response_bytes(wire.as_bytes(), Method::Get).unwrap_err();
1019        assert!(matches!(error, NanoGetError::MalformedHeader(_)));
1020    }
1021
1022    #[test]
1023    fn content_length_body_reader_handles_huge_lengths_without_preallocation() {
1024        let mut empty = io::empty();
1025        let error = read_content_length_body(&mut empty, usize::MAX).unwrap_err();
1026        assert!(matches!(error, NanoGetError::IncompleteMessage(_)));
1027    }
1028
1029    #[test]
1030    fn deterministic_response_parser_fuzz_harness_is_panic_free() {
1031        let corpus: &[&[u8]] = &[
1032            b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
1033            b"HTTP/1.1 204 No Content\r\n\r\n",
1034            b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n",
1035            b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello",
1036            b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\nok",
1037            b"HTP/1.1 200 OK\r\n\r\n",
1038            b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nzz\r\n",
1039            b"",
1040            b"HTTP/1.1 200 OK\r\nContent-Length: 999999999999999999999\r\n\r\n",
1041        ];
1042
1043        for seed in corpus {
1044            let run = panic::catch_unwind(AssertUnwindSafe(|| {
1045                let _ = parse_response_bytes(seed, Method::Get);
1046            }));
1047            assert!(run.is_ok(), "parser panicked on corpus input");
1048        }
1049
1050        let mut rng = DeterministicRng::new(0xC0FFEE_F00DBAAD);
1051        for _ in 0..3_000 {
1052            let mut bytes = rng.next_bytes(512);
1053            if !bytes.is_empty() && (rng.next_u32() & 1) == 0 {
1054                let idx = (rng.next_u32() as usize) % bytes.len();
1055                bytes[idx] = bytes[idx].wrapping_add(1);
1056            }
1057            let method = if (rng.next_u32() & 1) == 0 {
1058                Method::Get
1059            } else {
1060                Method::Head
1061            };
1062            let run = panic::catch_unwind(AssertUnwindSafe(|| {
1063                let _ = parse_response_bytes(&bytes, method);
1064            }));
1065            assert!(run.is_ok(), "parser panicked for fuzz case");
1066        }
1067    }
1068}