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::{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>`, populate `self.trailer` from any
146    /// chunked trailer headers, and close the body.
147    pub fn body_bytes(&mut self) -> Result<Vec<u8>, HttpError> {
148        let mut out = Vec::new();
149        if let Some(ref mut body) = self.body {
150            body.read_to_end(&mut out).map_err(|_| HttpError::BodyRead)?;
151        }
152        if let Some(body) = self.body.take() {
153            self.trailer = body.into_trailers();
154        }
155        Ok(out)
156    }
157
158    /// Read the body as a UTF-8 string.
159    pub fn body_string(&mut self) -> Result<String, HttpError> {
160        let bytes = self.body_bytes()?;
161        String::from_utf8(bytes)
162            .map_err(|_| HttpError::BodyRead)
163    }
164}
165
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    fn make_writer() -> ConnResponseWriter<Vec<u8>> {
172        ConnResponseWriter::new(Vec::new())
173    }
174
175    #[test]
176    fn default_200() {
177        let mut w = make_writer();
178        w.write(b"hello").unwrap();
179        w.finish().unwrap();
180        let out = String::from_utf8(w.inner).unwrap();
181        assert!(out.starts_with("HTTP/1.1 200 OK\r\n"));
182        assert!(out.contains("hello"));
183    }
184
185    #[test]
186    fn custom_status() {
187        let mut w = make_writer();
188        w.write_header(404);
189        w.write(b"not found").unwrap();
190        w.finish().unwrap();
191        let out = String::from_utf8(w.inner).unwrap();
192        assert!(out.starts_with("HTTP/1.1 404 Not Found\r\n"));
193    }
194
195    #[test]
196    fn write_header_idempotent() {
197        let mut w = make_writer();
198        w.write_header(201);
199        w.write(b"body").unwrap(); // triggers flush_headers with status 201
200        w.write_header(500);       // second call — ignored, headers already sent
201        w.finish().unwrap();
202        let out = String::from_utf8(w.inner).unwrap();
203        assert!(out.starts_with("HTTP/1.1 201"));
204    }
205}