Skip to main content

giong_http/
response.rs

1//! The response a handler returns, and its wire format.
2
3use crate::parse::{is_field_value, is_token_str};
4
5/// A response body.
6#[derive(Debug, Default)]
7pub enum Body {
8    /// No body.
9    #[default]
10    Empty,
11    /// Bytes that live for the whole program: sent without copying into a
12    /// fresh allocation first.
13    Static(&'static [u8]),
14    /// Owned bytes.
15    Owned(Vec<u8>),
16}
17
18impl Body {
19    fn as_bytes(&self) -> &[u8] {
20        match self {
21            Body::Empty => &[],
22            Body::Static(bytes) => bytes,
23            Body::Owned(bytes) => bytes,
24        }
25    }
26}
27
28impl From<&'static str> for Body {
29    fn from(s: &'static str) -> Self {
30        Body::Static(s.as_bytes())
31    }
32}
33
34impl From<&'static [u8]> for Body {
35    fn from(s: &'static [u8]) -> Self {
36        Body::Static(s)
37    }
38}
39
40impl From<String> for Body {
41    fn from(s: String) -> Self {
42        Body::Owned(s.into_bytes())
43    }
44}
45
46impl From<Vec<u8>> for Body {
47    fn from(v: Vec<u8>) -> Self {
48        Body::Owned(v)
49    }
50}
51
52/// An HTTP response.
53///
54/// ```
55/// use giong_http::Response;
56///
57/// let ok = Response::json(r#"{"ok":true}"#);
58/// let missing = Response::new(404).header("Cache-Control", "no-store").body("not found");
59/// # let _ = (ok, missing);
60/// ```
61#[derive(Debug)]
62pub struct Response {
63    status: u16,
64    /// Extra header lines, already in wire format.
65    headers: Vec<u8>,
66    body: Body,
67}
68
69impl Response {
70    /// A response with the given status code and no body.
71    ///
72    /// # Panics
73    ///
74    /// Panics if `status` is not in `200..=599`. Interim (1xx) responses are
75    /// sent by the server itself.
76    pub fn new(status: u16) -> Self {
77        assert!(
78            (200..=599).contains(&status),
79            "giong-http: invalid status code {status}"
80        );
81        Self {
82            status,
83            headers: Vec::new(),
84            body: Body::Empty,
85        }
86    }
87
88    /// `200 OK` with a `text/plain; charset=utf-8` body.
89    pub fn text(body: impl Into<Body>) -> Self {
90        Self::new(200)
91            .header("Content-Type", "text/plain; charset=utf-8")
92            .body(body)
93    }
94
95    /// `200 OK` with an `application/json` body.
96    pub fn json(body: impl Into<Body>) -> Self {
97        Self::new(200)
98            .header("Content-Type", "application/json")
99            .body(body)
100    }
101
102    /// Adds a header field.
103    ///
104    /// `Content-Length`, `Transfer-Encoding`, `Connection` and `Date` are
105    /// managed by the server; setting them here is ignored.
106    ///
107    /// # Panics
108    ///
109    /// Panics if `name` is not a valid header name or `value` contains control
110    /// characters. Building headers from untrusted input without checking it
111    /// would otherwise allow response splitting.
112    pub fn header(mut self, name: &str, value: &str) -> Self {
113        assert!(
114            is_token_str(name.as_bytes()),
115            "giong-http: invalid header name {name:?}"
116        );
117        assert!(
118            is_field_value(value.as_bytes()),
119            "giong-http: invalid value for header {name}: {value:?}"
120        );
121        let managed = ["content-length", "transfer-encoding", "connection", "date"];
122        if !managed.iter().any(|m| name.eq_ignore_ascii_case(m)) {
123            self.headers.extend_from_slice(name.as_bytes());
124            self.headers.extend_from_slice(b": ");
125            self.headers.extend_from_slice(value.trim().as_bytes());
126            self.headers.extend_from_slice(b"\r\n");
127        }
128        self
129    }
130
131    /// Sets the body.
132    pub fn body(mut self, body: impl Into<Body>) -> Self {
133        self.body = body.into();
134        self
135    }
136
137    /// The status code.
138    pub fn status(&self) -> u16 {
139        self.status
140    }
141
142    /// Appends the response in wire format to `out`.
143    pub(crate) fn write_to(&self, out: &mut Vec<u8>, minor: u8, keep_alive: bool, head_only: bool) {
144        let body = self.body.as_bytes();
145        out.reserve(128 + self.headers.len() + body.len());
146        out.extend_from_slice(if minor == 0 {
147            b"HTTP/1.0 "
148        } else {
149            b"HTTP/1.1 "
150        });
151        push_number(out, self.status as usize);
152        out.push(b' ');
153        out.extend_from_slice(reason(self.status).as_bytes());
154        out.extend_from_slice(b"\r\nDate: ");
155        out.extend_from_slice(&crate::date::now());
156        out.extend_from_slice(b"\r\n");
157        // 204 and 304 never carry a body, so they get no length either.
158        let bodiless = matches!(self.status, 204 | 304);
159        if !bodiless {
160            out.extend_from_slice(b"Content-Length: ");
161            push_number(out, body.len());
162            out.extend_from_slice(b"\r\n");
163        }
164        out.extend_from_slice(&self.headers);
165        if !keep_alive {
166            out.extend_from_slice(b"Connection: close\r\n");
167        } else if minor == 0 {
168            out.extend_from_slice(b"Connection: keep-alive\r\n");
169        }
170        out.extend_from_slice(b"\r\n");
171        if !head_only && !bodiless {
172            out.extend_from_slice(body);
173        }
174    }
175}
176
177fn push_number(out: &mut Vec<u8>, mut n: usize) {
178    let mut digits = [0u8; 20];
179    let mut at = digits.len();
180    loop {
181        at -= 1;
182        digits[at] = b'0' + (n % 10) as u8;
183        n /= 10;
184        if n == 0 {
185            break;
186        }
187    }
188    out.extend_from_slice(&digits[at..]);
189}
190
191pub(crate) fn reason(status: u16) -> &'static str {
192    match status {
193        200 => "OK",
194        201 => "Created",
195        202 => "Accepted",
196        204 => "No Content",
197        206 => "Partial Content",
198        301 => "Moved Permanently",
199        302 => "Found",
200        303 => "See Other",
201        304 => "Not Modified",
202        307 => "Temporary Redirect",
203        308 => "Permanent Redirect",
204        400 => "Bad Request",
205        401 => "Unauthorized",
206        403 => "Forbidden",
207        404 => "Not Found",
208        405 => "Method Not Allowed",
209        406 => "Not Acceptable",
210        408 => "Request Timeout",
211        409 => "Conflict",
212        410 => "Gone",
213        411 => "Length Required",
214        412 => "Precondition Failed",
215        413 => "Content Too Large",
216        414 => "URI Too Long",
217        415 => "Unsupported Media Type",
218        417 => "Expectation Failed",
219        422 => "Unprocessable Content",
220        429 => "Too Many Requests",
221        431 => "Request Header Fields Too Large",
222        500 => "Internal Server Error",
223        501 => "Not Implemented",
224        502 => "Bad Gateway",
225        503 => "Service Unavailable",
226        504 => "Gateway Timeout",
227        505 => "HTTP Version Not Supported",
228        _ => match status / 100 {
229            2 => "OK",
230            3 => "Redirection",
231            4 => "Client Error",
232            _ => "Server Error",
233        },
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    fn wire(resp: &Response, minor: u8, keep_alive: bool, head_only: bool) -> String {
242        let mut out = Vec::new();
243        resp.write_to(&mut out, minor, keep_alive, head_only);
244        let text = String::from_utf8(out).unwrap();
245        // The date changes; check its shape and cut it out.
246        let start = text.find("Date: ").unwrap();
247        let end = start + text[start..].find("\r\n").unwrap() + 2;
248        assert_eq!(end - start, 6 + 29 + 2);
249        format!("{}{}", &text[..start], &text[end..])
250    }
251
252    #[test]
253    fn serialises_status_headers_and_body() {
254        let resp = Response::json(r#"{"a":1}"#).header("X-Id", " 7 ");
255        assert_eq!(
256            wire(&resp, 1, true, false),
257            "HTTP/1.1 200 OK\r\nContent-Length: 7\r\nContent-Type: application/json\r\nX-Id: 7\r\n\r\n{\"a\":1}"
258        );
259        assert_eq!(
260            wire(&Response::new(404), 1, false, false),
261            "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
262        );
263        assert_eq!(
264            wire(&Response::text(vec![b'x'; 12345]), 0, true, true),
265            "HTTP/1.0 200 OK\r\nContent-Length: 12345\r\nContent-Type: text/plain; charset=utf-8\r\nConnection: keep-alive\r\n\r\n"
266        );
267        assert_eq!(
268            wire(&Response::new(204).body("ignored"), 1, true, false),
269            "HTTP/1.1 204 No Content\r\n\r\n"
270        );
271        assert_eq!(
272            wire(&Response::new(599), 1, true, false),
273            "HTTP/1.1 599 Server Error\r\nContent-Length: 0\r\n\r\n"
274        );
275    }
276
277    #[test]
278    fn managed_headers_cannot_be_overridden() {
279        let resp = Response::new(200)
280            .header("content-length", "999")
281            .header("Connection", "keep-alive")
282            .header("Transfer-Encoding", "chunked")
283            .header("Date", "yesterday")
284            .body("ok");
285        assert_eq!(
286            wire(&resp, 1, true, false),
287            "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok"
288        );
289    }
290
291    #[test]
292    #[should_panic(expected = "invalid value for header")]
293    fn header_values_cannot_split_the_response() {
294        let _ = Response::new(200).header("X-User", "a\r\nSet-Cookie: stolen=1");
295    }
296
297    #[test]
298    #[should_panic(expected = "invalid header name")]
299    fn header_names_must_be_tokens() {
300        let _ = Response::new(200).header("Bad Name", "1");
301    }
302
303    #[test]
304    #[should_panic(expected = "invalid status code")]
305    fn interim_status_codes_are_rejected() {
306        let _ = Response::new(100);
307    }
308}