Skip to main content

go_http/
response.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// Response and ResponseWriter — port of Go's `net/http.Response` and
4/// `net/http.ResponseWriter`.
5use std::io::{self, Read, Write};
6
7use crate::error::HttpError;
8use crate::header::Header;
9use crate::parse::transfer::Body;
10use crate::status;
11
12// ---------------------------------------------------------------------------
13// ResponseWriter — the server-side write interface
14// ---------------------------------------------------------------------------
15
16/// The interface a handler uses to construct an HTTP response.
17/// Port of Go's `http.ResponseWriter`.
18pub trait ResponseWriter: Send {
19    /// Access the response headers (call before `write_header` or first `write`).
20    fn header(&mut self) -> &mut Header;
21
22    /// Write body bytes. Implicitly calls `write_header(200)` on the first call.
23    fn write(&mut self, buf: &[u8]) -> Result<usize, HttpError>;
24
25    /// Send the status code and headers. Can only be called once; subsequent
26    /// calls are ignored.
27    fn write_header(&mut self, status_code: u16);
28}
29
30// ---------------------------------------------------------------------------
31// ConnResponseWriter — concrete ResponseWriter backed by a TCP stream
32// ---------------------------------------------------------------------------
33
34/// A `ResponseWriter` that writes to an arbitrary `Write` (a TCP connection).
35pub struct ConnResponseWriter<W: Write + Send> {
36    pub(crate) inner: W,
37    header:         Header,
38    status:         u16,
39    header_written: bool,
40    /// When `true`, use chunked framing for the body.
41    chunked:        bool,
42}
43
44impl<W: Write + Send> ConnResponseWriter<W> {
45    pub fn new(inner: W) -> Self {
46        let mut header = Header::new();
47        // Default headers mirroring Go's net/http defaults.
48        header.set("Content-Type", "text/plain; charset=utf-8");
49        Self {
50            inner,
51            header,
52            status: 200,
53            header_written: false,
54            chunked: true, // default: chunked unless Content-Length is set
55        }
56    }
57
58    fn flush_headers(&mut self) -> Result<(), HttpError> {
59        if self.header_written {
60            return Ok(());
61        }
62        self.header_written = true;
63
64        // Choose framing.
65        let has_cl = self.header.get("Content-Length").is_some();
66        if !has_cl {
67            self.header.set("Transfer-Encoding", "chunked");
68        }
69
70        let status_text = status::status_text(self.status);
71        write!(self.inner, "HTTP/1.1 {} {}\r\n", self.status, status_text)?;
72        self.header.write_to(&mut self.inner)?;
73        self.inner.write_all(b"\r\n")?;
74        Ok(())
75    }
76}
77
78impl<W: Write + Send> ResponseWriter for ConnResponseWriter<W> {
79    fn header(&mut self) -> &mut Header {
80        &mut self.header
81    }
82
83    fn write(&mut self, buf: &[u8]) -> Result<usize, HttpError> {
84        self.flush_headers()?;
85        if self.chunked && self.header.get("Content-Length").is_none() {
86            // Emit one chunk.
87            write!(self.inner, "{:X}\r\n", buf.len())?;
88            self.inner.write_all(buf)?;
89            self.inner.write_all(b"\r\n")?;
90            Ok(buf.len())
91        } else {
92            self.inner.write_all(buf)?;
93            Ok(buf.len())
94        }
95    }
96
97    fn write_header(&mut self, status_code: u16) {
98        if self.header_written {
99            return;
100        }
101        self.status = status_code;
102    }
103}
104
105impl<W: Write + Send> ConnResponseWriter<W> {
106    /// Finish the response: write the terminal chunk (if chunked) and flush.
107    pub fn finish(&mut self) -> Result<(), HttpError> {
108        self.flush_headers()?;
109        if self.chunked && self.header.get("Content-Length").is_none() {
110            self.inner.write_all(b"0\r\n\r\n")?;
111        }
112        self.inner.flush()?;
113        Ok(())
114    }
115}
116
117// ---------------------------------------------------------------------------
118// Response — the client-side response value
119// ---------------------------------------------------------------------------
120
121/// An HTTP response received by the client.
122/// Mirrors Go's `http.Response`.
123pub struct Response {
124    /// Status code, e.g. 200.
125    pub status: u16,
126    /// Reason phrase, e.g. "OK".
127    pub status_text: String,
128    /// Protocol version, e.g. "HTTP/1.1".
129    pub proto: String,
130    pub proto_major: u8,
131    pub proto_minor: u8,
132    /// Response headers.
133    pub header: Header,
134    /// Response body.
135    pub body: Option<Body>,
136    /// -1 if unknown.
137    pub content_length: i64,
138    /// Transfer-Encoding values.
139    pub transfer_encoding: Vec<String>,
140    /// Trailer headers populated after the body is fully consumed.
141    pub trailer: Header,
142}
143
144impl Response {
145    /// Read the entire body into a `Vec<u8>` and close it.
146    pub fn body_bytes(&mut self) -> Result<Vec<u8>, HttpError> {
147        let mut out = Vec::new();
148        if let Some(body) = self.body.take() {
149            BodyReader(body)
150                .read_to_end(&mut out)
151                .map_err(|_| HttpError::BodyRead)?;
152        }
153        Ok(out)
154    }
155
156    /// Read the body as a UTF-8 string.
157    pub fn body_string(&mut self) -> Result<String, HttpError> {
158        let bytes = self.body_bytes()?;
159        String::from_utf8(bytes)
160            .map_err(|_| HttpError::BodyRead)
161    }
162}
163
164struct BodyReader(Body);
165impl Read for BodyReader {
166    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
167        self.0.read(buf)
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    fn make_writer() -> ConnResponseWriter<Vec<u8>> {
176        ConnResponseWriter::new(Vec::new())
177    }
178
179    #[test]
180    fn default_200() {
181        let mut w = make_writer();
182        w.write(b"hello").unwrap();
183        w.finish().unwrap();
184        let out = String::from_utf8(w.inner).unwrap();
185        assert!(out.starts_with("HTTP/1.1 200 OK\r\n"));
186        assert!(out.contains("hello"));
187    }
188
189    #[test]
190    fn custom_status() {
191        let mut w = make_writer();
192        w.write_header(404);
193        w.write(b"not found").unwrap();
194        w.finish().unwrap();
195        let out = String::from_utf8(w.inner).unwrap();
196        assert!(out.starts_with("HTTP/1.1 404 Not Found\r\n"));
197    }
198
199    #[test]
200    fn write_header_idempotent() {
201        let mut w = make_writer();
202        w.write_header(201);
203        w.write(b"body").unwrap(); // triggers flush_headers with status 201
204        w.write_header(500);       // second call — ignored, headers already sent
205        w.finish().unwrap();
206        let out = String::from_utf8(w.inner).unwrap();
207        assert!(out.starts_with("HTTP/1.1 201"));
208    }
209}