Skip to main content

go_http/parse/
transfer.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// Body framing — port of Go net/http `readTransfer` / `writeTransfer`.
4///
5/// Resolves whether a message has a body and how it is framed (chunked vs
6/// content-length vs connection-close), then wraps the raw reader in the
7/// appropriate limiting/decoding reader.
8use std::io::{self, Read, Take};
9
10use super::{chunk::ChunkedReader, ParseError};
11use crate::header::Header;
12
13// ---------------------------------------------------------------------------
14// Body — the opaque body reader type
15// ---------------------------------------------------------------------------
16
17/// An HTTP message body.
18pub enum Body {
19    /// Body of exactly `n` bytes.
20    Limited(Take<Box<dyn Read + Send>>),
21    /// Chunked transfer-encoded body.
22    Chunked(ChunkedReader<Box<dyn Read + Send>>),
23    /// Body read until connection close (response only).
24    Unbounded(Box<dyn Read + Send>),
25    /// No body (HEAD, 204, 304, …).
26    Empty,
27}
28
29impl Read for Body {
30    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
31        match self {
32            Self::Limited(r)   => r.read(buf),
33            Self::Chunked(r)   => r.read(buf),
34            Self::Unbounded(r) => r.read(buf),
35            Self::Empty        => Ok(0),
36        }
37    }
38}
39
40impl Body {
41    /// Read the entire body into a `Vec<u8>`.
42    ///
43    /// Used by the HTTP client to buffer the response body before releasing
44    /// the underlying `TcpStream` back to the connection pool.  Buffering
45    /// first ensures the stream is fully drained and that no `try_clone()`
46    /// alias of the stream is live when the next request starts, which would
47    /// cause concurrent reads on the same socket and SIGSEGV on Linux/epoll.
48    pub fn read_to_vec(&mut self) -> io::Result<Vec<u8>> {
49        let mut buf = Vec::new();
50        self.read_to_end(&mut buf)?;
51        Ok(buf)
52    }
53}
54
55// ---------------------------------------------------------------------------
56// Body presence / framing resolution
57// ---------------------------------------------------------------------------
58
59/// Who is asking: determines whether "no Content-Length, no TE" means a body
60/// (responses: read until close) or no body (requests).
61#[derive(Clone, Copy, PartialEq, Eq)]
62pub enum MessageKind {
63    Request,
64    Response { status: u16, method: Option<RequestMethod> },
65}
66
67/// The request method that prompted a response (used to decide body for HEAD).
68#[derive(Clone, Copy, PartialEq, Eq)]
69pub enum RequestMethod {
70    Head,
71    Connect,
72    Other,
73}
74
75/// Resolve the body reader for a message given its headers and kind.
76///
77/// Port of Go's `readTransfer`: chooses chunked, content-length, unbounded, or
78/// empty framing, then wraps `r` accordingly.
79///
80/// Returns `(body, trailers_key)` — `trailers_key` is `true` if the `Trailer`
81/// header was present, signaling that the caller should harvest trailer headers
82/// from the chunked reader after reading is complete.
83pub fn resolve_body(
84    r: Box<dyn Read + Send>,
85    headers: &Header,
86    kind: MessageKind,
87) -> Result<Body, ParseError> {
88    // RFC 7230 §3.3: certain responses never have a body.
89    if let MessageKind::Response { status, method } = kind {
90        let no_body = status == 204
91            || status == 304
92            || (100..200).contains(&status)
93            || method == Some(RequestMethod::Head)
94            || method == Some(RequestMethod::Connect) && (200..300).contains(&status);
95        if no_body {
96            return Ok(Body::Empty);
97        }
98    }
99
100    // Check Transfer-Encoding.
101    let te = headers.get("Transfer-Encoding").unwrap_or("").to_ascii_lowercase();
102    if te.contains("chunked") {
103        return Ok(Body::Chunked(ChunkedReader::new(r)));
104    }
105
106    // Check Content-Length.
107    if let Some(cl_str) = headers.get("Content-Length") {
108        let n: u64 = cl_str
109            .trim()
110            .parse()
111            .map_err(|_| ParseError::InvalidContentLength)?;
112        return Ok(Body::Limited(r.take(n)));
113    }
114
115    // For requests with no framing: no body.
116    // For responses with no framing: read until EOF.
117    match kind {
118        MessageKind::Request => Ok(Body::Empty),
119        MessageKind::Response { .. } => Ok(Body::Unbounded(r)),
120    }
121}
122
123/// Serialize body framing headers for an outgoing message.
124///
125/// Port of Go's `writeTransfer`: if the body length is known, writes
126/// `Content-Length`; otherwise selects chunked encoding and writes
127/// `Transfer-Encoding: chunked`.
128pub fn write_framing_headers(
129    headers: &mut Header,
130    content_length: Option<u64>,
131) {
132    if let Some(n) = content_length {
133        headers.set("Content-Length", n.to_string());
134        headers.del("Transfer-Encoding");
135    } else {
136        headers.set("Transfer-Encoding", "chunked");
137        headers.del("Content-Length");
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use std::io::{Cursor, Read};
145
146    fn boxed(b: &'static [u8]) -> Box<dyn Read + Send> {
147        Box::new(Cursor::new(b))
148    }
149
150    #[test]
151    fn content_length_body() {
152        let mut h = Header::new();
153        h.set("Content-Length", "5");
154        let mut body = resolve_body(boxed(b"Hello World"), &h, MessageKind::Request).unwrap();
155        let mut out = Vec::new();
156        body.read_to_end(&mut out).unwrap();
157        assert_eq!(out, b"Hello");
158    }
159
160    #[test]
161    fn chunked_body() {
162        let raw: &'static [u8] = b"5\r\nHello\r\n0\r\n\r\n";
163        let mut h = Header::new();
164        h.set("Transfer-Encoding", "chunked");
165        let mut body = resolve_body(boxed(raw), &h, MessageKind::Request).unwrap();
166        let mut out = Vec::new();
167        body.read_to_end(&mut out).unwrap();
168        assert_eq!(out, b"Hello");
169    }
170
171    #[test]
172    fn no_content_request() {
173        let h = Header::new();
174        let mut body = resolve_body(boxed(b"leftover"), &h, MessageKind::Request).unwrap();
175        let mut out = Vec::new();
176        body.read_to_end(&mut out).unwrap();
177        assert!(out.is_empty());
178    }
179
180    #[test]
181    fn no_body_204() {
182        let h = Header::new();
183        let mut body = resolve_body(
184            boxed(b"ignored"),
185            &h,
186            MessageKind::Response { status: 204, method: None },
187        )
188        .unwrap();
189        let mut out = Vec::new();
190        body.read_to_end(&mut out).unwrap();
191        assert!(out.is_empty());
192    }
193
194    #[test]
195    fn write_framing_content_length() {
196        let mut h = Header::new();
197        write_framing_headers(&mut h, Some(42));
198        assert_eq!(h.get("Content-Length"), Some("42"));
199        assert_eq!(h.get("Transfer-Encoding"), None);
200    }
201
202    #[test]
203    fn write_framing_chunked() {
204        let mut h = Header::new();
205        write_framing_headers(&mut h, None);
206        assert_eq!(h.get("Transfer-Encoding"), Some("chunked"));
207        assert_eq!(h.get("Content-Length"), None);
208    }
209}