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}