Skip to main content

go_http/parse/
response.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// HTTP/1.1 response parser — port of Go net/http `ReadResponse`.
4use std::io::Read;
5
6use super::{read_headers, read_line, ParseError};
7use crate::header::Header;
8use crate::parse::transfer::{resolve_body, Body, MessageKind, RequestMethod};
9
10/// The result of parsing an HTTP/1.1 response line + headers.
11pub struct ParsedResponse {
12    pub proto:             String,
13    pub proto_major:       u8,
14    pub proto_minor:       u8,
15    pub status:            u16,
16    pub status_text:       String,
17    pub header:            Header,
18    pub body:              Body,
19    pub content_length:    i64,
20    pub transfer_encoding: Vec<String>,
21}
22
23impl std::fmt::Debug for ParsedResponse {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("ParsedResponse")
26            .field("status", &self.status)
27            .field("status_text", &self.status_text)
28            .finish_non_exhaustive()
29    }
30}
31
32/// Parse an HTTP/1.1 response from `r`.
33///
34/// `req_method` is the method of the request that prompted this response —
35/// needed to decide body presence for HEAD and CONNECT.
36///
37/// Port of Go's `ReadResponse(r *bufio.Reader, req *Request) (*Response, error)`.
38pub fn read_response(
39    r: impl Read + Send + 'static,
40    req_method: Option<&str>,
41    max_header_bytes: usize,
42) -> Result<ParsedResponse, ParseError> {
43    let mut r: Box<dyn Read + Send> = Box::new(r);
44
45    // ── Status line ──────────────────────────────────────────────────────────
46    let line = read_line(&mut r)?;
47
48    // "HTTP/1.1 200 OK" — the reason phrase is optional (HTTP/2 drops it).
49    let sp1 = line.find(' ').ok_or(ParseError::BadStatusLine)?;
50    let proto = &line[..sp1];
51    let rest = line[sp1 + 1..].trim_start();
52
53    let (proto_major, proto_minor) = parse_proto(proto)?;
54
55    let (status_str, status_text) = match rest.find(' ') {
56        Some(i) => (&rest[..i], rest[i + 1..].to_owned()),
57        None    => (rest, String::new()),
58    };
59
60    let status: u16 = status_str
61        .parse()
62        .map_err(|_| ParseError::BadStatusLine)?;
63
64    if !(100..=999).contains(&status) {
65        return Err(ParseError::BadStatusLine);
66    }
67
68    // ── Headers ───────────────────────────────────────────────────────────────
69    let header = read_headers(&mut r, max_header_bytes)?;
70
71    // ── Transfer-Encoding list ────────────────────────────────────────────────
72    let transfer_encoding: Vec<String> = header
73        .values("Transfer-Encoding")
74        .iter()
75        .flat_map(|v| v.split(',').map(|s| s.trim().to_ascii_lowercase()))
76        .collect();
77
78    // ── Content-Length ────────────────────────────────────────────────────────
79    let content_length = match header.get("Content-Length") {
80        None    => -1,
81        Some(s) => s.trim().parse::<i64>().map_err(|_| ParseError::InvalidContentLength)?,
82    };
83
84    // ── Body ──────────────────────────────────────────────────────────────────
85    let method = match req_method.map(|m| m.to_ascii_uppercase()).as_deref() {
86        Some("HEAD")    => Some(RequestMethod::Head),
87        Some("CONNECT") => Some(RequestMethod::Connect),
88        _               => Some(RequestMethod::Other),
89    };
90
91    let body = resolve_body(
92        r,
93        &header,
94        MessageKind::Response { status, method },
95    )?;
96
97    Ok(ParsedResponse {
98        proto: proto.to_owned(),
99        proto_major,
100        proto_minor,
101        status,
102        status_text,
103        header,
104        body,
105        content_length,
106        transfer_encoding,
107    })
108}
109
110// ---------------------------------------------------------------------------
111// Helpers
112// ---------------------------------------------------------------------------
113
114fn parse_proto(proto: &str) -> Result<(u8, u8), ParseError> {
115    if !proto.starts_with("HTTP/") {
116        return Err(ParseError::BadStatusLine);
117    }
118    let ver = &proto[5..];
119    let dot = ver.find('.').ok_or(ParseError::BadStatusLine)?;
120    let major: u8 = ver[..dot].parse().map_err(|_| ParseError::BadStatusLine)?;
121    let minor: u8 = ver[dot + 1..].parse().map_err(|_| ParseError::BadStatusLine)?;
122    Ok((major, minor))
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use std::io::{Cursor, Read};
129
130    fn parse(raw: &'static [u8], method: Option<&str>) -> ParsedResponse {
131        read_response(Cursor::new(raw), method, 65536).unwrap()
132    }
133
134    #[test]
135    fn simple_200() {
136        let resp = parse(
137            b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello",
138            Some("GET"),
139        );
140        assert_eq!(resp.status, 200);
141        assert_eq!(resp.status_text, "OK");
142        assert_eq!(resp.proto_major, 1);
143        assert_eq!(resp.proto_minor, 1);
144        assert_eq!(resp.content_length, 5);
145    }
146
147    #[test]
148    fn head_response_no_body() {
149        let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n";
150        let mut resp = read_response(Cursor::new(raw.as_ref()), Some("HEAD"), 65536).unwrap();
151        let mut out = Vec::new();
152        resp.body.read_to_end(&mut out).unwrap();
153        assert!(out.is_empty());
154    }
155
156    #[test]
157    fn no_content_204() {
158        let raw = b"HTTP/1.1 204 No Content\r\n\r\n";
159        let mut resp = parse(raw, Some("DELETE"));
160        let mut out = Vec::new();
161        resp.body.read_to_end(&mut out).unwrap();
162        assert!(out.is_empty());
163    }
164
165    #[test]
166    fn chunked_response() {
167        let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\n\r\n";
168        let mut resp = parse(raw, Some("GET"));
169        let mut out = Vec::new();
170        resp.body.read_to_end(&mut out).unwrap();
171        assert_eq!(out, b"Hello");
172    }
173
174    #[test]
175    fn bad_status_code() {
176        let result = read_response(
177            Cursor::new(b"HTTP/1.1 999 Bad\r\n\r\n"),
178            None,
179            65536,
180        );
181        // 999 is technically in range 100–999 per our check, so OK; spot-check
182        // a truly invalid one:
183        assert!(result.is_ok(), "status 999 should be valid");
184        let result2 = read_response(
185            Cursor::new(b"HTTP/1.1 abc Bad\r\n\r\n"),
186            None,
187            65536,
188        );
189        assert_eq!(result2.unwrap_err(), ParseError::BadStatusLine);
190    }
191}