dhttp/reqres/
res.rs

1//! HTTP response and its constructors
2
3use blake3_lite::Hasher;
4use percent_encoding_lite::{is_encoded, encode, Bitmask};
5
6use crate::reqres::{HttpRequest, HttpHeader, HttpBody, StatusCode};
7use crate::reqres::sse::HttpSse;
8
9/// Your response
10#[derive(Debug)]
11#[non_exhaustive]
12pub struct HttpResponse {
13    pub code: StatusCode,
14    pub headers: Vec<HttpHeader>,
15    pub body: HttpBody,
16    pub content_type: String,
17}
18
19impl HttpResponse {
20    /// An empty response
21    pub fn new() -> HttpResponse {
22        HttpResponse::with_type("", vec![])
23    }
24
25    /// Pushes a new header
26    pub fn add_header(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut HttpResponse {
27        self.headers.push(HttpHeader { name: name.into(), value: value.into() });
28        self
29    }
30
31    /// Constructs new response with a specified `Content-Type`
32    pub fn with_type(content_type: impl Into<String>, body: impl Into<HttpBody>) -> HttpResponse {
33        HttpResponse {
34            code: StatusCode::OK,
35            headers: vec![],
36            body: body.into(),
37            content_type: content_type.into(),
38        }
39    }
40}
41
42impl Default for HttpResponse {
43    fn default() -> HttpResponse {
44        HttpResponse::new()
45    }
46}
47
48/// Response of bytes (`application/octet-stream`)
49pub fn bytes(bytes: Vec<u8>) -> HttpResponse {
50    HttpResponse::with_type("application/octet-stream", bytes)
51}
52
53/// Plaintext response (`text/plain`)
54pub fn text(text: impl Into<String>) -> HttpResponse {
55    HttpResponse::with_type("text/plain; charset=utf-8", text.into())
56}
57
58/// HTML response (`text/html`)
59///
60/// Also stamps an ETag to enable caching
61pub fn html(req: &HttpRequest, html: impl Into<String>) -> HttpResponse {
62    let html = html.into();
63
64    let mut hasher = Hasher::new();
65    hasher.update(html.as_bytes());
66    // You'd probably execute me for that but I see no security flaws to truncate Blake3 for caching purposes
67    let mut hash = [0; 8];
68    hasher.finalize(&mut hash);
69    let hex = format!("\"{}\"", crate::util::hex(&hash));
70
71    let mut res = HttpResponse::with_type("text/html; charset=utf-8", html);
72    if req.cmp_header("If-None-Match", &hex) {
73        res.code = StatusCode::NOT_MODIFIED;
74        res.body = HttpBody::Empty;
75    }
76    res.add_header("ETag", hex);
77    res
78}
79
80/// JSON response (`application/json`)
81pub fn json(json: impl Into<String>) -> HttpResponse {
82    HttpResponse::with_type("application/json", json.into())
83}
84
85/// HTTP redirect with the `Location` header
86pub fn redirect(dest: impl Into<String>) -> HttpResponse {
87    let mut dest = dest.into();
88    // To avoid XSS for URLs containing a double quote or back slash
89    if !is_encoded(&dest, Bitmask::URI) {
90        dest = encode(dest, Bitmask::URI);
91    }
92    HttpResponse {
93        code: StatusCode::MOVED_PERMANENTLY,
94        headers: vec![HttpHeader { name: "Location".to_string(), value: dest.clone() }],
95        body: format!("<a href=\"{dest}\">Click here if you weren't redirected</a>\n").into(),
96        content_type: "text/html; charset=utf-8".to_string(),
97    }
98}
99
100pub fn sse(handler: impl HttpSse) -> HttpResponse {
101    HttpResponse::with_type("text/event-stream", HttpBody::Upgrade(Box::new(handler)))
102}
103
104pub use super::file::file;