body_image_futio/
lib.rs

1//! Asynchronous HTTP integration for _body-image_.
2//!
3//! The _body-image-futio_ crate integrates _body-image_ with
4//! _futures_, _http_, _hyper_, and _tokio_ for both client and server
5//! use.
6//!
7//! * Trait [`RequestRecorder`] extends `http::request::Builder` for recording
8//!   a [`RequestRecord`] of various body types, which can then be passed to
9//!   `request_dialog` or `fetch`.
10//!
11//! * The [`fetch()`] function runs a `RequestRecord` and returns a completed
12//!   [`Dialog`](body_image::Dialog) using a single-use client and runtime for
13//!   `request_dialog`.
14//!
15//! * The [`request_dialog()`] function returns a `Future` with `Dialog`
16//!   output, given a suitable `hyper::Client` reference and
17//!   `RequestRecord`. This function is thus more composable for complete
18//!   _tokio_ applications.
19//!
20//! * [`AsyncBodyImage`] adapts a `BodyImage` for asynchronous output as a
21//!   `Stream` and `http_body::Body`.
22//!
23//! * [`AsyncBodySink`] adapts a `BodySink` for asynchronous input from a
24//!   (e.g. `hyper::Body`) `Stream`.
25//!
26//! * The [`decode_res_body()`] and associated functions will decompress any
27//!   supported Transfer/Content-Encoding of the response body and update the
28//!   `Dialog` accordingly.
29//!
30//! ## Optional Features
31//!
32//! The following features may be enabled or disabled at build time. All are
33//! enabled by default.
34//!
35//! _mmap:_ Adds zero-copy memory map support, via a [`UniBodyBuf`] type usable
36//! with all `Stream` and `Sink` types.
37//!
38//! _brotli:_ Adds the brotli compression algorithm to [`ACCEPT_ENCODINGS`] and
39//! decompression support in [`decode_res_body`].
40//!
41//! _hyper-http_: Adds Hyper based [`fetch()`] and [`request_dialog()`]
42//! functions, as well as a [`RequestRecorder`] implementation for
43//! `hyper::Body` (its "default" `http_body::Body` type).
44
45#![warn(rust_2018_idioms)]
46
47use std::error::Error as StdError;
48use std::fmt;
49use std::io;
50use std::time::Duration;
51
52use tao_log::warn;
53
54use body_image::{
55    BodyImage, BodyError, Encoding,
56    Prolog, RequestRecorded,
57};
58
59#[cfg(feature = "hyper-http")]
60use body_image::{Epilog, Dialog, BodySink};
61
62/// Conveniently compact type alias for dyn Trait `std::error::Error`.
63///
64/// It is possible to query and downcast the type via methods of
65/// [`std::any::Any`].
66pub type Flaw = Box<dyn StdError + Send + Sync + 'static>;
67
68mod blocking;
69pub use blocking::{Blocking, BlockingArbiter, LenientArbiter, StatefulArbiter};
70
71mod decode;
72pub use decode::{decode_res_body, find_encodings, find_chunked};
73
74#[cfg(feature = "hyper-http")] mod fetch;
75#[cfg(feature = "hyper-http")] pub use self::fetch::{fetch, request_dialog};
76
77mod tune;
78pub use tune::{BlockingPolicy, FutioTunables, FutioTuner};
79
80mod sink;
81pub use sink::{InputBuf, AsyncBodySink, DispatchBodySink, PermitBodySink};
82
83mod stream;
84pub use stream::{
85    AsyncBodyImage, OutputBuf,
86    DispatchBodyImage, PermitBodyImage,
87    SplitBodyImage, YieldBodyImage,
88};
89
90#[cfg(feature = "mmap")] mod mem_map_buf;
91#[cfg(feature = "mmap")] use mem_map_buf::MemMapBuf;
92
93mod uni_body_buf;
94pub use uni_body_buf::UniBodyBuf;
95
96mod wrappers;
97pub use wrappers::{SinkWrapper, StreamWrapper, RequestRecorder};
98
99/// The crate version string.
100pub static VERSION: &str = env!("CARGO_PKG_VERSION");
101
102/// Appropriate value for the HTTP accept-encoding request header, including
103/// (br)otli when the brotli feature is configured.
104#[cfg(feature = "brotli")]
105pub static ACCEPT_ENCODINGS: &str = "br, gzip, deflate";
106
107/// Appropriate value for the HTTP accept-encoding request header, including
108/// (br)otli when the brotli feature is configured.
109#[cfg(not(feature = "brotli"))]
110pub static ACCEPT_ENCODINGS: &str = "gzip, deflate";
111
112/// A browser-like HTTP accept request header value, with preference for
113/// hypertext.
114pub static BROWSE_ACCEPT: &str =
115    "text/html, application/xhtml+xml, \
116     application/xml;q=0.9, \
117     */*;q=0.8";
118
119/// Error enumeration for body-image-futio origin errors. This may be
120/// extended in the future so exhaustive matching is gently discouraged with
121/// an unused variant.
122#[derive(Debug)]
123pub enum FutioError {
124    /// Error from `BodySink` or `BodyImage`.
125    Body(BodyError),
126
127    /// The `FutioTunables::res_timeout` duration was reached before receiving
128    /// the initial response.
129    ResponseTimeout(Duration),
130
131    /// The `FutioTunables::body_timeout` duration was reached before receiving
132    /// the complete response body.
133    BodyTimeout(Duration),
134
135    /// The content-length header exceeded `Tunables::max_body`.
136    ContentLengthTooLong(u64),
137
138    /// Error from _http_.
139    Http(http::Error),
140
141    /// Error from _hyper_.
142    #[cfg(feature = "hyper-http")]
143    Hyper(hyper::Error),
144
145    /// Failed to decode an unsupported `Encoding`; such as `Compress`, or
146    /// `Brotli`, when the _brotli_ feature is not enabled.
147    UnsupportedEncoding(Encoding),
148
149    /// A pending blocking permit or dispatched operation was canceled
150    /// before it was granted or completed.
151    OpCanceled(blocking_permit::Canceled),
152
153    /// Other unclassified errors.
154    Other(Flaw),
155
156    /// Unused variant to both enable non-exhaustive matching and warn against
157    /// exhaustive matching.
158    _FutureProof
159}
160
161impl fmt::Display for FutioError {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match self {
164            FutioError::Body(ref be) =>
165                write!(f, "With body: {}", be),
166            FutioError::ResponseTimeout(d) =>
167                write!(f, "Timeout before initial response ({:?})", d),
168            FutioError::BodyTimeout(d) =>
169                write!(f, "Timeout before streaming body complete ({:?})", d),
170            FutioError::ContentLengthTooLong(l) =>
171                write!(f, "Response Content-Length too long: {}", l),
172            FutioError::Http(ref e) =>
173                write!(f, "Http error: {}", e),
174            #[cfg(feature = "hyper-http")]
175            FutioError::Hyper(ref e) =>
176                write!(f, "Hyper error: {}", e),
177            FutioError::UnsupportedEncoding(e) =>
178                write!(f, "Unsupported encoding: {}", e),
179            FutioError::OpCanceled(e) =>
180                write!(f, "Error: {}", e),
181            FutioError::Other(ref flaw) =>
182                write!(f, "Other error: {}", flaw),
183            FutioError::_FutureProof =>
184                unreachable!("Don't abuse the _FutureProof!")
185        }
186    }
187}
188
189impl StdError for FutioError {
190    fn source(&self) -> Option<&(dyn StdError + 'static)> {
191        match *self {
192            FutioError::Body(ref be)         => Some(be),
193            FutioError::Http(ref ht)         => Some(ht),
194            #[cfg(feature = "hyper-http")]
195            FutioError::Hyper(ref he)        => Some(he),
196            FutioError::OpCanceled(ref ce)   => Some(ce),
197            FutioError::Other(ref flaw)      => Some(flaw.as_ref()),
198            _ => None
199        }
200    }
201}
202
203impl From<BodyError> for FutioError {
204    fn from(err: BodyError) -> FutioError {
205        FutioError::Body(err)
206    }
207}
208
209impl From<http::Error> for FutioError {
210    fn from(err: http::Error) -> FutioError {
211        FutioError::Http(err)
212    }
213}
214
215#[cfg(feature = "hyper-http")]
216impl From<hyper::Error> for FutioError {
217    fn from(err: hyper::Error) -> FutioError {
218        FutioError::Hyper(err)
219    }
220}
221
222impl From<io::Error> for FutioError {
223    fn from(err: io::Error) -> FutioError {
224        FutioError::Body(BodyError::Io(err))
225    }
226}
227
228// Note: There is intentionally no `From` implementation for `Flaw` to
229// `FutioError::Other`, as that could easily lead to misclassification.
230// Instead it should be manually constructed.
231
232/// Return a generic HTTP user-agent header value for the crate, with version
233pub fn user_agent() -> String {
234    format!("Mozilla/5.0 (compatible; body-image {}; \
235             +https://crates.io/crates/body-image)",
236            VERSION)
237}
238
239/// An `http::Request` and recording.
240///
241/// Note that other important getter methods for `RequestRecord` are found in
242/// trait implementation [`RequestRecorded`](#impl-RequestRecorded).
243///
244/// _Limitations:_ This can't be `Clone`, because `http::Request` currently
245/// isn't `Clone`.  Also note that as used as type `B`, `hyper::Body` also
246/// isn't `Clone`.
247#[derive(Debug)]
248pub struct RequestRecord<B> {
249    request:      http::Request<B>,
250    prolog:       Prolog,
251}
252
253impl<B> RequestRecord<B> {
254    /// The HTTP method (verb), e.g. `GET`, `POST`, etc.
255    pub fn method(&self)  -> &http::Method         { &self.prolog.method }
256
257    /// The complete URL as used in the request.
258    pub fn url(&self)     -> &http::Uri            { &self.prolog.url }
259
260    /// Return the HTTP request.
261    pub fn request(&self) -> &http::Request<B>     { &self.request }
262}
263
264impl<B> RequestRecorded for RequestRecord<B> {
265    fn req_headers(&self) -> &http::HeaderMap      { &self.prolog.req_headers }
266    fn req_body(&self)    -> &BodyImage            { &self.prolog.req_body }
267}
268
269/// Temporary `http::Response` wrapper, with preserved request
270/// recording.
271#[cfg(feature = "hyper-http")]
272#[derive(Debug)]
273struct Monolog {
274    prolog:       Prolog,
275    response:     http::Response<hyper::Body>,
276}
277
278/// An HTTP request with response in progress of being received.
279#[derive(Debug)]
280#[cfg(feature = "hyper-http")]
281struct InDialog {
282    prolog:       Prolog,
283    version:      http::Version,
284    status:       http::StatusCode,
285    res_headers:  http::HeaderMap,
286    res_body:     BodySink,
287}
288
289#[cfg(feature = "hyper-http")]
290impl InDialog {
291    // Convert to `Dialog` by preparing the response body and adding an
292    // initial res_decoded for Chunked, if hyper handled chunked transfer
293    // encoding.
294    fn prepare(self) -> Result<Dialog, FutioError> {
295        let res_decoded = if find_chunked(&self.res_headers) {
296            vec![Encoding::Chunked]
297        } else {
298            Vec::with_capacity(0)
299        };
300
301        Ok(Dialog::new(
302            self.prolog,
303            Epilog {
304                version:     self.version,
305                status:      self.status,
306                res_headers: self.res_headers,
307                res_body:    self.res_body.prepare()?,
308                res_decoded,
309            }
310        ))
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    mod forward;
317
318    #[cfg(feature = "hyper-http")]
319    mod server;
320
321    /// These tests may fail because they depend on public web servers
322    #[cfg(all(feature = "hyper-http", feature = "may-fail"))]
323    mod live;
324
325    use tao_log::{debug, debugv};
326    use super::{FutioError, Flaw};
327    use piccolog::test_logger;
328    use std::mem::size_of;
329
330    fn is_flaw(f: Flaw) -> bool {
331        debug!("Flaw Debug: {:?}, Display: \"{}\"", f, f);
332        true
333    }
334
335    #[test]
336    fn test_error_as_flaw() {
337        assert!(test_logger());
338        assert!(is_flaw(FutioError::ContentLengthTooLong(42).into()));
339        assert!(is_flaw(FutioError::Other("one off".into()).into()));
340    }
341
342    #[test]
343    fn test_error_size() {
344        assert!(test_logger());
345        assert!(debugv!(size_of::<FutioError>()) <= 32);
346    }
347
348    #[test]
349    #[should_panic]
350    fn test_error_future_proof() {
351        assert!(!FutioError::_FutureProof.to_string().is_empty(),
352                "should have panic'd before, unreachable")
353    }
354}