mco_http/client/
mod.rs

1//! HTTP Client
2//!
3//! # Usage
4//!
5//! The `Client` API is designed for most people to make HTTP requests.
6//! It utilizes the lower level `Request` API.
7//!
8//! ## GET
9//!
10//! ```no_run
11//! # use mco_http::Client;
12//! let client = Client::new();
13//!
14//! let res = client.get("http://example.domain").send().unwrap();
15//! assert_eq!(res.status, mco_http::Ok);
16//! ```
17//!
18//! The returned value is a `Response`, which provides easy access to
19//! the `status`, the `headers`, and the response body via the `Read`
20//! trait.
21//!
22//! ## POST
23//!
24//! ```no_run
25//! # use mco_http::Client;
26//! let client = Client::new();
27//!
28//! let res = client.post("http://example.domain")
29//!     .body("foo=bar")
30//!     .send()
31//!     .unwrap();
32//! assert_eq!(res.status, mco_http::Ok);
33//! ```
34//!
35//! # Sync
36//!
37//! The `Client` implements `Sync`, so you can share it among multiple threads
38//! and make multiple requests simultaneously.
39//!
40//! ```no_run
41//! # use mco_http::Client;
42//! use std::sync::Arc;
43//! use std::thread;
44//!
45//! // Note: an Arc is used here because `thread::spawn` creates threads that
46//! // can outlive the main thread, so we must use reference counting to keep
47//! // the Client alive long enough. Scoped threads could skip the Arc.
48//! let client = Arc::new(Client::new());
49//! let clone1 = client.clone();
50//! let clone2 = client.clone();
51//! thread::spawn(move || {
52//!     clone1.get("http://example.domain").send().unwrap();
53//! });
54//! thread::spawn(move || {
55//!     clone2.post("http://example.domain/post").body("foo=bar").send().unwrap();
56//! });
57//! ```
58use std::borrow::Cow;
59use std::default::Default;
60use std::io::{self, copy, Read};
61use std::fmt;
62
63use std::time::Duration;
64
65use url::Url;
66use url::ParseError as UrlError;
67
68use crate::header::{Headers, Header, HeaderFormat};
69use crate::header::{ContentLength, Host, Location};
70use crate::method::Method;
71use crate::net::{NetworkConnector, NetworkStream, SslClient};
72use crate::Error;
73
74use self::proxy::{Proxy, tunnel};
75use self::scheme::Scheme;
76pub use self::pool::Pool;
77pub use self::request::Request;
78pub use self::response::Response;
79
80mod proxy;
81pub mod pool;
82pub mod request;
83pub mod response;
84
85use crate::http::Protocol;
86use crate::http::h1::Http11Protocol;
87
88
89/// A Client to use additional features with Requests.
90///
91/// Clients can handle things such as: redirect policy, connection pooling.
92pub struct Client {
93    protocol: Box<dyn Protocol + Send + Sync>,
94    redirect_policy: RedirectPolicy,
95    read_timeout: Option<Duration>,
96    write_timeout: Option<Duration>,
97    proxy: Option<(Scheme, Cow<'static, str>, u16)>
98}
99
100impl fmt::Debug for Client {
101    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
102        fmt.debug_struct("Client")
103           .field("redirect_policy", &self.redirect_policy)
104           .field("read_timeout", &self.read_timeout)
105           .field("write_timeout", &self.write_timeout)
106           .field("proxy", &self.proxy)
107           .finish()
108    }
109}
110
111impl Client {
112
113    /// Create a new Client.
114    pub fn new() -> Client {
115        Client::with_pool_config(Default::default())
116    }
117
118    /// Create a new Client with a configured Pool Config.
119    pub fn with_pool_config(config: pool::Config) -> Client {
120        Client::with_connector(Pool::new(config))
121    }
122
123    /// Create a Client with an HTTP proxy to a (host, port).
124    pub fn with_http_proxy<H>(host: H, port: u16) -> Client
125    where H: Into<Cow<'static, str>> {
126        let host = host.into();
127        let proxy = tunnel((Scheme::Http, host.clone(), port));
128        let mut client = Client::with_connector(Pool::with_connector(Default::default(), proxy));
129        client.proxy = Some((Scheme::Http, host, port));
130        client
131    }
132
133    /// Create a Client using a proxy with a custom connector and SSL client.
134    pub fn with_proxy_config<C, S>(proxy_config: ProxyConfig<C, S>) -> Client
135    where C: NetworkConnector + Send + Sync + 'static,
136          C::Stream: NetworkStream + Send + Clone,
137          S: SslClient<C::Stream> + Send + Sync + 'static {
138
139        let scheme = proxy_config.scheme;
140        let host = proxy_config.host;
141        let port = proxy_config.port;
142        let proxy = Proxy {
143            proxy: (scheme.clone(), host.clone(), port),
144            connector: proxy_config.connector,
145            ssl: proxy_config.ssl,
146        };
147
148        let mut client = match proxy_config.pool_config {
149            Some(pool_config) => Client::with_connector(Pool::with_connector(pool_config, proxy)),
150            None => Client::with_connector(proxy),
151        };
152        client.proxy = Some((scheme, host, port));
153        client
154    }
155
156    /// Create a new client with a specific connector.
157    pub fn with_connector<C, S>(connector: C) -> Client
158    where C: NetworkConnector<Stream=S> + Send + Sync + 'static, S: NetworkStream + Send {
159        Client::with_protocol(Http11Protocol::with_connector(connector))
160    }
161
162    /// Create a new client with a specific `Protocol`.
163    pub fn with_protocol<P: Protocol + Send + Sync + 'static>(protocol: P) -> Client {
164        Client {
165            protocol: Box::new(protocol),
166            redirect_policy: Default::default(),
167            read_timeout: None,
168            write_timeout: None,
169            proxy: None,
170        }
171    }
172
173    /// Set the RedirectPolicy.
174    pub fn set_redirect_policy(&mut self, policy: RedirectPolicy) {
175        self.redirect_policy = policy;
176    }
177
178    /// Set the read timeout value for all requests.
179    pub fn set_read_timeout(&mut self, dur: Option<Duration>) {
180        self.read_timeout = dur;
181    }
182
183    /// Set the write timeout value for all requests.
184    pub fn set_write_timeout(&mut self, dur: Option<Duration>) {
185        self.write_timeout = dur;
186    }
187
188    /// Build a Get request.
189    pub fn get<U: IntoUrl>(&self, url: U) -> RequestBuilder {
190        self.request(Method::Get, url)
191    }
192
193    /// Build a Head request.
194    pub fn head<U: IntoUrl>(&self, url: U) -> RequestBuilder {
195        self.request(Method::Head, url)
196    }
197
198    /// Build a Patch request.
199    pub fn patch<U: IntoUrl>(&self, url: U) -> RequestBuilder {
200        self.request(Method::Patch, url)
201    }
202
203    /// Build a Post request.
204    pub fn post<U: IntoUrl>(&self, url: U) -> RequestBuilder {
205        self.request(Method::Post, url)
206    }
207
208    /// Build a Put request.
209    pub fn put<U: IntoUrl>(&self, url: U) -> RequestBuilder {
210        self.request(Method::Put, url)
211    }
212
213    /// Build a Delete request.
214    pub fn delete<U: IntoUrl>(&self, url: U) -> RequestBuilder {
215        self.request(Method::Delete, url)
216    }
217
218
219    /// Build a new request using this Client.
220    pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
221        RequestBuilder {
222            client: self,
223            method: method,
224            url: url.into_url(),
225            body: None,
226            headers: None,
227        }
228    }
229}
230
231impl Default for Client {
232    fn default() -> Client { Client::new() }
233}
234
235/// Options for an individual Request.
236///
237/// One of these will be built for you if you use one of the convenience
238/// methods, such as `get()`, `post()`, etc.
239pub struct RequestBuilder<'a> {
240    client: &'a Client,
241    // We store a result here because it's good to keep RequestBuilder
242    // from being generic, but it is a nicer API to report the error
243    // from `send` (when other errors may be happening, so it already
244    // returns a `Result`). Why's it good to keep it non-generic? It
245    // stops downstream crates having to remonomorphise and recompile
246    // the code, which can take a while, since `send` is fairly large.
247    // (For an extreme example, a tiny crate containing
248    // `mco_http::Client::new().get("x").send().unwrap();` took ~4s to
249    // compile with a generic RequestBuilder, but 2s with this scheme,)
250    url: Result<Url, UrlError>,
251    headers: Option<Headers>,
252    method: Method,
253    body: Option<Body<'a>>,
254}
255
256impl<'a> RequestBuilder<'a> {
257
258    /// Set a request body to be sent.
259    pub fn body<B: Into<Body<'a>>>(mut self, body: B) -> RequestBuilder<'a> {
260        self.body = Some(body.into());
261        self
262    }
263
264    /// Add additional headers to the request.
265    pub fn headers(mut self, headers: Headers) -> RequestBuilder<'a> {
266        self.headers = Some(headers);
267        self
268    }
269
270    /// Add an individual new header to the request.
271    pub fn header<H: Header + HeaderFormat>(mut self, header: H) -> RequestBuilder<'a> {
272        {
273            let headers = match self.headers {
274                Some(ref mut h) => h,
275                None => {
276                    self.headers = Some(Headers::new());
277                    self.headers.as_mut().unwrap()
278                }
279            };
280
281            headers.set(header);
282        }
283        self
284    }
285
286    /// Execute this request and receive a Response back.
287    pub fn send(self) -> crate::Result<Response> {
288        let RequestBuilder { client, method, url, headers, body } = self;
289        let mut url = url?;
290        trace!("send method={:?}, url={:?}, client={:?}", method, url, client);
291
292        let can_have_body = match method {
293            Method::Get | Method::Head => false,
294            _ => true
295        };
296
297        let mut body = if can_have_body {
298            body
299        } else {
300            None
301        };
302
303        loop {
304            let mut req = {
305                let (host, port) = get_host_and_port(&url)?;
306                let mut message = client.protocol.new_message(&host, port, url.scheme())?;
307                if url.scheme() == "http" && client.proxy.is_some() {
308                    message.set_proxied(true);
309                }
310
311                let mut h = Headers::new();
312                h.set(Host {
313                    hostname: host.to_owned(),
314                    port: Some(port),
315                });
316                if let Some(ref headers) = headers {
317                    h.extend(headers.iter());
318                }
319                let headers = h;
320                Request::with_headers_and_message(method.clone(), url.clone(), headers, message)
321            };
322
323            req.set_write_timeout(client.write_timeout)?;
324            req.set_read_timeout(client.read_timeout)?;
325
326            match (can_have_body, body.as_ref()) {
327                (true, Some(body)) => match body.size() {
328                    Some(size) => req.headers_mut().set(ContentLength(size)),
329                    None => (), // chunked, Request will add it automatically
330                },
331                (true, None) => req.headers_mut().set(ContentLength(0)),
332                _ => () // neither
333            }
334            let mut streaming = req.start()?;
335            if let Some(mut rdr) = body.take() {
336                copy(&mut rdr, &mut streaming)?;
337            }
338            let res = streaming.send()?;
339            if !res.status.is_redirection() {
340                return Ok(res)
341            }
342            debug!("redirect code {:?} for {}", res.status, url);
343
344            let loc = {
345                // punching borrowck here
346                let loc = match res.headers.get::<Location>() {
347                    Some(&Location(ref loc)) => {
348                        Some(url.join(loc))
349                    }
350                    None => {
351                        debug!("no Location header");
352                        // could be 304 Not Modified?
353                        None
354                    }
355                };
356                match loc {
357                    Some(r) => r,
358                    None => return Ok(res)
359                }
360            };
361            url = match loc {
362                Ok(u) => u,
363                Err(e) => {
364                    debug!("Location header had invalid URI: {:?}", e);
365                    return Ok(res);
366                }
367            };
368            match client.redirect_policy {
369                // separate branches because they can't be one
370                RedirectPolicy::FollowAll => (), //continue
371                RedirectPolicy::FollowIf(cond) if cond(&url) => (), //continue
372                _ => return Ok(res),
373            }
374        }
375    }
376}
377
378/// An enum of possible body types for a Request.
379pub enum Body<'a> {
380    /// A Reader does not necessarily know it's size, so it is chunked.
381    ChunkedBody(&'a mut (dyn Read + 'a)),
382    /// For Readers that can know their size, like a `File`.
383    SizedBody(&'a mut (dyn Read + 'a), u64),
384    /// A String has a size, and uses Content-Length.
385    BufBody(&'a [u8] , usize),
386}
387
388impl<'a> Body<'a> {
389    fn size(&self) -> Option<u64> {
390        match *self {
391            Body::SizedBody(_, len) => Some(len),
392            Body::BufBody(_, len) => Some(len as u64),
393            _ => None
394        }
395    }
396}
397
398impl<'a> Read for Body<'a> {
399    #[inline]
400    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
401        match *self {
402            Body::ChunkedBody(ref mut r) => r.read(buf),
403            Body::SizedBody(ref mut r, _) => r.read(buf),
404            Body::BufBody(ref mut r, _) => Read::read(r, buf),
405        }
406    }
407}
408
409impl<'a> Into<Body<'a>> for &'a [u8] {
410    #[inline]
411    fn into(self) -> Body<'a> {
412        Body::BufBody(self, self.len())
413    }
414}
415
416impl<'a> Into<Body<'a>> for &'a str {
417    #[inline]
418    fn into(self) -> Body<'a> {
419        self.as_bytes().into()
420    }
421}
422
423impl<'a> Into<Body<'a>> for &'a String {
424    #[inline]
425    fn into(self) -> Body<'a> {
426        self.as_bytes().into()
427    }
428}
429
430impl<'a, R: Read> From<&'a mut R> for Body<'a> {
431    #[inline]
432    fn from(r: &'a mut R) -> Body<'a> {
433        Body::ChunkedBody(r)
434    }
435}
436
437/// A helper trait to convert common objects into a Url.
438pub trait IntoUrl {
439    /// Consumes the object, trying to return a Url.
440    fn into_url(self) -> Result<Url, UrlError>;
441}
442
443impl IntoUrl for Url {
444    fn into_url(self) -> Result<Url, UrlError> {
445        Ok(self)
446    }
447}
448
449impl<'a> IntoUrl for &'a str {
450    fn into_url(self) -> Result<Url, UrlError> {
451        Url::parse(self)
452    }
453}
454
455impl<'a> IntoUrl for &'a String {
456    fn into_url(self) -> Result<Url, UrlError> {
457        Url::parse(self)
458    }
459}
460
461/// Proxy server configuration with a custom connector and TLS wrapper.
462pub struct ProxyConfig<C, S>
463where C: NetworkConnector + Send + Sync + 'static,
464      C::Stream: NetworkStream + Clone + Send,
465      S: SslClient<C::Stream> + Send + Sync + 'static {
466    scheme: Scheme,
467    host: Cow<'static, str>,
468    port: u16,
469    pool_config: Option<pool::Config>,
470    connector: C,
471    ssl: S,
472}
473
474impl<C, S> ProxyConfig<C, S>
475where C: NetworkConnector + Send + Sync + 'static,
476      C::Stream: NetworkStream + Clone + Send,
477      S: SslClient<C::Stream> + Send + Sync + 'static {
478
479    /// Create a new `ProxyConfig`.
480    #[inline]
481    pub fn new<H: Into<Cow<'static, str>>>(scheme: &str, host: H, port: u16, connector: C, ssl: S) -> ProxyConfig<C, S> {
482        ProxyConfig {
483            scheme: scheme.into(),
484            host: host.into(),
485            port: port,
486            pool_config: Some(pool::Config::default()),
487            connector: connector,
488            ssl: ssl,
489        }
490    }
491
492    /// Change the `pool::Config` for the proxy.
493    ///
494    /// Passing `None` disables the `Pool`.
495    ///
496    /// The default is enabled, with the default `pool::Config`.
497    pub fn set_pool_config(&mut self, pool_config: Option<pool::Config>) {
498        self.pool_config = pool_config;
499    }
500}
501
502/// Behavior regarding how to handle redirects within a Client.
503#[derive(Copy)]
504pub enum RedirectPolicy {
505    /// Don't follow any redirects.
506    FollowNone,
507    /// Follow all redirects.
508    FollowAll,
509    /// Follow a redirect if the contained function returns true.
510    FollowIf(fn(&Url) -> bool),
511}
512
513impl fmt::Debug for RedirectPolicy {
514    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
515        match *self {
516            RedirectPolicy::FollowNone => fmt.write_str("FollowNone"),
517            RedirectPolicy::FollowAll => fmt.write_str("FollowAll"),
518            RedirectPolicy::FollowIf(_) => fmt.write_str("FollowIf"),
519        }
520    }
521}
522
523// This is a hack because of upstream typesystem issues.
524impl Clone for RedirectPolicy {
525    fn clone(&self) -> RedirectPolicy {
526        *self
527    }
528}
529
530impl Default for RedirectPolicy {
531    fn default() -> RedirectPolicy {
532        RedirectPolicy::FollowAll
533    }
534}
535
536
537fn get_host_and_port(url: &Url) -> crate::Result<(&str, u16)> {
538    let host = match url.host_str() {
539        Some(host) => host,
540        None => return Err(Error::Uri(UrlError::EmptyHost)),
541    };
542    trace!("host={:?}", host);
543    let port = match url.port_or_known_default() {
544        Some(port) => port,
545        None => return Err(Error::Uri(UrlError::InvalidPort)),
546    };
547    trace!("port={:?}", port);
548    Ok((host, port))
549}
550
551mod scheme {
552
553    #[derive(Clone, PartialEq, Eq, Debug, Hash)]
554    pub enum Scheme {
555        Http,
556        Https,
557        Other(String),
558    }
559
560    impl<'a> From<&'a str> for Scheme {
561        fn from(s: &'a str) -> Scheme {
562            match s {
563                "http" => Scheme::Http,
564                "https" => Scheme::Https,
565                s => Scheme::Other(String::from(s)),
566            }
567        }
568    }
569
570    impl AsRef<str> for Scheme {
571        fn as_ref(&self) -> &str {
572            match *self {
573                Scheme::Http => "http",
574                Scheme::Https => "https",
575                Scheme::Other(ref s) => s,
576            }
577        }
578    }
579
580}
581
582#[cfg(test)]
583mod tests {
584    use std::io::Read;
585    use crate::header::Server;
586    use crate::http::h1::Http11Message;
587    use crate::mock::{MockStream, MockSsl};
588    use super::{Client, RedirectPolicy};
589    use super::scheme::Scheme;
590    use super::proxy::Proxy;
591    use super::pool::Pool;
592    use url::Url;
593
594    mock_connector!(MockRedirectPolicy {
595        "http://127.0.0.1" =>       "HTTP/1.1 301 Redirect\r\n\
596                                     Location: http://127.0.0.2\r\n\
597                                     Server: mock1\r\n\
598                                     \r\n\
599                                    "
600        "http://127.0.0.2" =>       "HTTP/1.1 302 Found\r\n\
601                                     Location: https://127.0.0.3\r\n\
602                                     Server: mock2\r\n\
603                                     \r\n\
604                                    "
605        "https://127.0.0.3" =>      "HTTP/1.1 200 OK\r\n\
606                                     Server: mock3\r\n\
607                                     \r\n\
608                                    "
609    });
610
611
612    #[test]
613    fn test_proxy() {
614        use super::pool::PooledStream;
615        type MessageStream = PooledStream<super::proxy::Proxied<MockStream, MockStream>>;
616        mock_connector!(ProxyConnector {
617            b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
618        });
619        let tunnel = Proxy {
620            connector: ProxyConnector,
621            proxy: (Scheme::Http, "example.proxy".into(), 8008),
622            ssl: MockSsl,
623        };
624        let mut client = Client::with_connector(Pool::with_connector(Default::default(), tunnel));
625        client.proxy = Some((Scheme::Http, "example.proxy".into(), 8008));
626        let mut dump = vec![];
627        client.get("http://127.0.0.1/foo/bar").send().unwrap().read_to_end(&mut dump).unwrap();
628
629        let box_message = client.protocol.new_message("127.0.0.1", 80, "http").unwrap();
630        let message = box_message.downcast::<Http11Message>().unwrap();
631        let stream =  message.into_inner().downcast::<MessageStream>().unwrap().into_inner().into_normal().unwrap();
632
633        let s = ::std::str::from_utf8(&stream.write).unwrap();
634        let request_line = "GET http://127.0.0.1/foo/bar HTTP/1.1\r\n";
635        assert!(s.starts_with(request_line), "{:?} doesn't start with {:?}", s, request_line);
636        assert!(s.contains("Host: 127.0.0.1\r\n"));
637    }
638
639    #[test]
640    fn test_proxy_tunnel() {
641        use super::pool::PooledStream;
642        type MessageStream = PooledStream<super::proxy::Proxied<MockStream, MockStream>>;
643
644        mock_connector!(ProxyConnector {
645            b"HTTP/1.1 200 OK\r\n\r\n",
646            b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
647        });
648        let tunnel = Proxy {
649            connector: ProxyConnector,
650            proxy: (Scheme::Http, "example.proxy".into(), 8008),
651            ssl: MockSsl,
652        };
653        let mut client = Client::with_connector(Pool::with_connector(Default::default(), tunnel));
654        client.proxy = Some((Scheme::Http, "example.proxy".into(), 8008));
655        let mut dump = vec![];
656        client.get("https://127.0.0.1/foo/bar").send().unwrap().read_to_end(&mut dump).unwrap();
657
658        let box_message = client.protocol.new_message("127.0.0.1", 443, "https").unwrap();
659        let message = box_message.downcast::<Http11Message>().unwrap();
660        let stream = message.into_inner().downcast::<MessageStream>().unwrap().into_inner().into_tunneled().unwrap();
661
662        let s = ::std::str::from_utf8(&stream.write).unwrap();
663        let connect_line = "CONNECT 127.0.0.1:443 HTTP/1.1\r\nHost: 127.0.0.1:443\r\n\r\n";
664        assert_eq!(&s[..connect_line.len()], connect_line);
665
666        let s = &s[connect_line.len()..];
667        let request_line = "GET /foo/bar HTTP/1.1\r\n";
668        assert_eq!(&s[..request_line.len()], request_line);
669        assert!(s.contains("Host: 127.0.0.1\r\n"));
670    }
671
672    #[test]
673    fn test_redirect_followall() {
674        let mut client = Client::with_connector(MockRedirectPolicy);
675        client.set_redirect_policy(RedirectPolicy::FollowAll);
676
677        let res = client.get("http://127.0.0.1").send().unwrap();
678        assert_eq!(res.headers.get(), Some(&Server("mock3".to_owned())));
679    }
680
681    #[test]
682    fn test_redirect_dontfollow() {
683        let mut client = Client::with_connector(MockRedirectPolicy);
684        client.set_redirect_policy(RedirectPolicy::FollowNone);
685        let res = client.get("http://127.0.0.1").send().unwrap();
686        assert_eq!(res.headers.get(), Some(&Server("mock1".to_owned())));
687    }
688
689    #[test]
690    fn test_redirect_followif() {
691        fn follow_if(url: &Url) -> bool {
692            !url.as_str().contains("127.0.0.3")
693        }
694        let mut client = Client::with_connector(MockRedirectPolicy);
695        client.set_redirect_policy(RedirectPolicy::FollowIf(follow_if));
696        let res = client.get("http://127.0.0.1").send().unwrap();
697        assert_eq!(res.headers.get(), Some(&Server("mock2".to_owned())));
698    }
699
700    mock_connector!(Issue640Connector {
701        b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\n",
702        b"GET",
703        b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\n",
704        b"HEAD",
705        b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\n",
706        b"POST"
707    });
708
709    // see issue #640
710    #[test]
711    fn test_head_response_body_keep_alive() {
712        let client = Client::with_connector(Pool::with_connector(Default::default(), Issue640Connector));
713
714        let mut s = String::new();
715        client.get("http://127.0.0.1").send().unwrap().read_to_string(&mut s).unwrap();
716        assert_eq!(s, "GET");
717
718        let mut s = String::new();
719        client.head("http://127.0.0.1").send().unwrap().read_to_string(&mut s).unwrap();
720        assert_eq!(s, "");
721
722        let mut s = String::new();
723        client.post("http://127.0.0.1").send().unwrap().read_to_string(&mut s).unwrap();
724        assert_eq!(s, "POST");
725    }
726
727    #[test]
728    fn test_request_body_error_is_returned() {
729        mock_connector!(Connector {
730            b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n",
731            b"HELLO"
732        });
733
734        struct BadBody;
735
736        impl ::std::io::Read for BadBody {
737            fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
738                Err(std::io::Error::new(std::io::ErrorKind::Other, "BadBody read"))
739            }
740        }
741
742        let client = Client::with_connector(Connector);
743        let err = client.post("http://127.0.0.1").body(&mut BadBody).send().unwrap_err();
744        assert_eq!(err.to_string(), "BadBody read");
745    }
746}