hreq/server/
resb_ext.rs

1//! Extension trait for `http::request::Builder`
2
3use crate::params::{AutoCharset, HReqParams};
4use crate::Body;
5use encoding_rs::Encoding;
6use http::response;
7use http::Response;
8use serde::Serialize;
9use std::time::Duration;
10
11/// Extends [`http::response::Builder`] with ergonomic extras for hreq.
12///
13/// These extensions are part of the primary goal of hreq to provide a "User first API".
14///
15/// [`http::response::Builder`]: https://docs.rs/http/latest/http/response/struct.Builder.html
16pub trait ResponseBuilderExt
17where
18    Self: Sized,
19{
20    /// Set a timeout for the response, including sending the body.
21    ///
22    /// If the timeout is reached, the current operation is aborted with a 500.
23    ///
24    /// ```
25    /// use hreq::prelude::*;
26    /// use std::time::Duration;
27    ///
28    /// async fn handle(req: http::Request<hreq::Body>) -> http::Response<&'static str> {
29    ///     http::Response::builder()
30    ///         .timeout(Duration::from_nanos(1))
31    ///         .body("Hello World!")
32    ///         .unwrap()
33    /// }
34    /// ```
35    ///
36    /// [`Error::Io`]: enum.Error.html#variant.Io
37    /// [`Error::is_timeout()`]: enum.Error.html#method.is_timeout
38    fn timeout(self, duration: Duration) -> Self;
39
40    /// This is an alias for `.timeout()` without having to construct a `Duration`.
41    ///
42    /// ```
43    /// use hreq::prelude::*;
44    ///
45    /// async fn handle(req: http::Request<hreq::Body>) -> http::Response<&'static str> {
46    ///     http::Response::builder()
47    ///         .timeout_millis(10_000)
48    ///         .body("Hello World!")
49    ///         .unwrap()
50    /// }
51    /// ```
52    fn timeout_millis(self, millis: u64) -> Self;
53
54    /// Toggle automatic response body charset encoding. Defaults to `true`.
55    ///
56    /// hreq encodes the response body of text MIME types according to the `charset` in
57    /// the `content-type` response header:
58    ///
59    ///   * `content-type: text/html; charset=iso8859-1`
60    ///
61    /// The behavior is triggered for any MIME type starting with `text/`. Because we're in rust,
62    /// there's an underlying assumption that the source of the response body is in `utf-8`,
63    /// but this can be changed using [`charset_encode_source`].
64    ///
65    /// Setting this to `false` disables any automatic charset encoding of the response body.
66    ///
67    /// # Examples
68    ///
69    /// You have plain text in a rust String (which is always utf-8) and you want an
70    /// http response with `iso8859-1` (aka `latin-1`). The default assumption
71    /// is that the source is in `utf-8`. You only need a `content-type` header.
72    ///
73    /// ```
74    /// use hreq::prelude::*;
75    ///
76    /// async fn handle(req: http::Request<hreq::Body>) -> http::Response<&'static str> {
77    ///     // This is a &str in rust default utf-8
78    ///     let content = "Und in die Bäumen hängen Löwen und Bären";
79    ///
80    ///     http::Response::builder()
81    ///         .header("content-type", "text/html; charset=iso8859-1")
82    ///         .body(content)
83    ///         .unwrap()
84    /// }
85    /// ```
86    ///
87    /// Or if you have a plain text file in utf-8.
88    ///
89    /// ```
90    /// use hreq::prelude::*;
91    /// use std::fs::File;
92    ///
93    /// #[cfg(feature = "tokio")]
94    /// async fn handle(req: http::Request<hreq::Body>) -> http::Response<std::fs::File> {
95    ///     http::Response::builder()
96    ///         // This header converts the body to iso8859-1
97    ///         .header("content-type", "text/plain; charset=iso8859-1")
98    ///         .body(File::open("my-utf8-file.txt").unwrap())
99    ///         .unwrap()
100    /// }
101    /// ```
102    ///
103    /// [`charset_encode_source`]: trait.ResponseBuilderExt.html#tymethod.charset_encode_source
104    fn charset_encode(self, enable: bool) -> Self;
105
106    /// Sets how to interpret response body source. Defaults to `utf-8`.
107    ///
108    /// When doing charset conversion of the response body, this set how to interpret the
109    /// source of the body.
110    ///
111    /// The setting works together with the mechanic described in [`charset_encode`], i.e.
112    /// it is triggered by the presence of a `charset` part in a `content-type` request header
113    /// with a `text` MIME.
114    ///
115    ///   * `content-type: text/html; charset=iso8859-1`
116    ///
117    /// Notice if the [`Body`] is a rust `String` or `&str`, this setting is ignored since
118    /// the internal represenation is always `utf-8`.
119    ///
120    /// ```
121    /// use hreq::prelude::*;
122    ///
123    /// async fn handle(req: http::Request<hreq::Body>) -> http::Response<Vec<u8>> {
124    ///     // おはよう世界 in EUC-JP.
125    ///     let euc_jp = vec![164_u8, 170, 164, 207, 164, 232, 164, 166, 192, 164, 179, 166];
126    ///
127    ///     http::Response::builder()
128    ///         // This header converts the body from EUC-JP to Shift-JIS
129    ///         .charset_encode_source("EUC-JP")
130    ///         .header("content-type", "text/html; charset=Shift_JIS")
131    ///         .body(euc_jp)
132    ///         .unwrap()
133    /// }
134    /// ```
135    ///
136    /// [`charset_encode`]: trait.ResponseBuilderExt.html#tymethod.charset_encode
137    /// [`Body`]: struct.Body.html
138    fn charset_encode_source(self, encoding: &str) -> Self;
139
140    /// Whether to use the `content-encoding` response header. Defaults to `true`.
141    ///
142    /// By default hreq encodes compressed body data automatically. The behavior is
143    /// triggered by setting the response header `content-encoding: gzip`.
144    ///
145    /// If the body data provided to hreq is already compressed we might need turn off
146    /// this default behavior.
147    ///
148    /// ```
149    /// use hreq::prelude::*;
150    ///
151    /// async fn handle(req: http::Request<hreq::Body>) -> http::Response<Vec<u8>> {
152    ///     // imagine we got some already gzip compressed data
153    ///     let already_compressed: Vec<u8> = vec![];
154    ///
155    ///     http::Response::builder()
156    ///         .header("content-encoding", "gzip")
157    ///         .content_encode(false) // do not do extra encoding
158    ///         .body(already_compressed)
159    ///         .unwrap()
160    /// }
161    /// ```
162    fn content_encode(self, enabled: bool) -> Self;
163
164    /// Toggle ability to read the response body into memory.
165    ///
166    /// When sending a response body, it's usually a good idea to read the entire body
167    /// (up to some limit) into memory. Doing so avoids using transfer-encoding chunked
168    /// when the content length can be determined.
169    ///
170    /// By default, hreq will attempt to prebuffer up to 256kb response body.
171    ///
172    /// Use this toggle to turn this behavior off.
173    fn prebuffer_response_body(self, enable: bool) -> Self;
174
175    /// Finish building the response by providing an object serializable to JSON.
176    ///
177    /// Objects made serializable with serde_derive can be automatically turned into
178    /// bodies. This sets both `content-type` and `content-length`.
179    ///
180    /// # Example
181    ///
182    /// ```
183    /// use hreq::prelude::*;
184    /// use hreq::Body;
185    /// use serde_derive::Serialize;
186    ///
187    /// #[derive(Serialize)]
188    /// struct MyJsonThing {
189    ///   name: String,
190    ///   age: String,
191    /// }
192    ///
193    /// async fn handle(req: http::Request<Body>) -> http::Response<Body> {
194    ///     let json = MyJsonThing {
195    ///         name: "Karl Kajal".into(),
196    ///         age: "32".into(),
197    ///     };
198    ///
199    ///     http::Response::builder()
200    ///         .with_json(&json)
201    ///         .unwrap()
202    /// }
203    /// ```
204    fn with_json<B: Serialize + ?Sized>(self, body: &B) -> http::Result<Response<Body>>;
205}
206
207impl ResponseBuilderExt for response::Builder {
208    fn timeout(self, duration: Duration) -> Self {
209        with_hreq_params(self, |params| {
210            params.timeout = Some(duration);
211        })
212    }
213
214    fn timeout_millis(self, millis: u64) -> Self {
215        self.timeout(Duration::from_millis(millis))
216    }
217
218    fn charset_encode(self, enable: bool) -> Self {
219        with_hreq_params(self, |params| {
220            params.charset_tx.toggle_target(enable);
221        })
222    }
223
224    fn charset_encode_source(self, encoding: &str) -> Self {
225        with_hreq_params(self, |params| {
226            let enc = Encoding::for_label(encoding.as_bytes());
227            if enc.is_none() {
228                warn!("Unknown character encoding: {}", encoding);
229            }
230            params.charset_tx.source = AutoCharset::Set(enc.unwrap());
231        })
232    }
233
234    fn content_encode(self, enable: bool) -> Self {
235        with_hreq_params(self, |params| {
236            params.content_encode = enable;
237        })
238    }
239
240    fn prebuffer_response_body(self, enable: bool) -> Self {
241        with_hreq_params(self, |params| {
242            params.prebuffer = enable;
243        })
244    }
245
246    fn with_json<B: Serialize + ?Sized>(self, body: &B) -> http::Result<Response<Body>> {
247        let body = Body::from_json(body);
248        self.body(body)
249    }
250}
251
252fn get_or_insert<T: Send + Sync + 'static, F: FnOnce() -> T>(
253    builder: &mut response::Builder,
254    f: F,
255) -> &mut T {
256    let ext = builder.extensions_mut().expect("Unwrap extensions");
257    if ext.get::<T>().is_none() {
258        ext.insert(f());
259    }
260    ext.get_mut::<T>().unwrap()
261}
262
263fn with_hreq_params<F: FnOnce(&mut HReqParams)>(
264    mut builder: response::Builder,
265    f: F,
266) -> response::Builder {
267    let params = get_or_insert(&mut builder, HReqParams::new);
268    f(params);
269    builder
270}