feat(http): allow specifying custom body streams

This commit is contained in:
Sean McArthur
2017-02-16 12:39:50 -08:00
parent 2266d869ca
commit 1b1311a7d3
11 changed files with 384 additions and 276 deletions

View File

@@ -6,10 +6,11 @@
use std::cell::RefCell;
use std::fmt;
use std::io;
use std::marker::PhantomData;
use std::rc::Rc;
use std::time::Duration;
use futures::{Poll, Async, Future};
use futures::{Poll, Async, Future, Stream};
use relay;
use tokio::io::Io;
use tokio::reactor::Handle;
@@ -37,14 +38,21 @@ mod response;
/// A Client to make outgoing HTTP requests.
// If the Connector is clone, then the Client can be clone easily.
#[derive(Clone)]
pub struct Client<C> {
pub struct Client<C, B = http::Body> {
connector: C,
handle: Handle,
pool: Pool<TokioClient>,
pool: Pool<TokioClient<B>>,
}
impl Client<HttpConnector> {
impl Client<HttpConnector, http::Body> {
/// Create a new Client with the default config.
#[inline]
pub fn new(handle: &Handle) -> Client<HttpConnector, http::Body> {
Config::default().build(handle)
}
}
impl Client<HttpConnector, http::Body> {
/// Configure a Client.
///
/// # Example
@@ -63,30 +71,28 @@ impl Client<HttpConnector> {
/// # }
/// ```
#[inline]
pub fn configure() -> Config<UseDefaultConnector> {
pub fn configure() -> Config<UseDefaultConnector, http::Body> {
Config::default()
}
}
impl Client<HttpConnector> {
/// Create a new Client with the default config.
#[inline]
pub fn new(handle: &Handle) -> Client<HttpConnector> {
Client::configure().build(handle)
}
}
impl<C: Connect> Client<C> {
impl<C, B> Client<C, B> {
/// Create a new client with a specific connector.
#[inline]
fn configured(config: Config<C>, handle: &Handle) -> Client<C> {
fn configured(config: Config<C, B>, handle: &Handle) -> Client<C, B> {
Client {
connector: config.connector,
handle: handle.clone(),
pool: Pool::new(config.keep_alive, config.keep_alive_timeout),
}
}
}
impl<C, B> Client<C, B>
where C: Connect,
B: Stream<Error=::Error> + 'static,
B::Item: AsRef<[u8]>,
{
/// Send a GET Request using this Client.
#[inline]
pub fn get(&self, url: Url) -> FutureResponse {
@@ -95,7 +101,7 @@ impl<C: Connect> Client<C> {
/// Send a constructed Request using this Client.
#[inline]
pub fn request(&self, req: Request) -> FutureResponse {
pub fn request(&self, req: Request<B>) -> FutureResponse {
self.call(req)
}
}
@@ -118,13 +124,17 @@ impl Future for FutureResponse {
}
}
impl<C: Connect> Service for Client<C> {
type Request = Request;
impl<C, B> Service for Client<C, B>
where C: Connect,
B: Stream<Error=::Error> + 'static,
B::Item: AsRef<[u8]>,
{
type Request = Request<B>;
type Response = Response;
type Error = ::Error;
type Future = FutureResponse;
fn call(&self, req: Request) -> Self::Future {
fn call(&self, req: Self::Request) -> Self::Future {
let url = req.url().clone();
let (mut head, body) = request::split(req);
let mut headers = Headers::new();
@@ -178,26 +188,40 @@ impl<C: Connect> Service for Client<C> {
}
impl<C> fmt::Debug for Client<C> {
impl<C: Clone, B> Clone for Client<C, B> {
fn clone(&self) -> Client<C, B> {
Client {
connector: self.connector.clone(),
handle: self.handle.clone(),
pool: self.pool.clone(),
}
}
}
impl<C, B> fmt::Debug for Client<C, B> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.pad("Client")
}
}
type TokioClient = ClientProxy<Message<http::RequestHead, TokioBody>, Message<http::ResponseHead, TokioBody>, ::Error>;
type TokioClient<B> = ClientProxy<Message<http::RequestHead, B>, Message<http::ResponseHead, TokioBody>, ::Error>;
struct HttpClient {
client_rx: RefCell<Option<relay::Receiver<Pooled<TokioClient>>>>,
struct HttpClient<B> {
client_rx: RefCell<Option<relay::Receiver<Pooled<TokioClient<B>>>>>,
}
impl<T: Io + 'static> ClientProto<T> for HttpClient {
impl<T, B> ClientProto<T> for HttpClient<B>
where T: Io + 'static,
B: Stream<Error=::Error> + 'static,
B::Item: AsRef<[u8]>,
{
type Request = http::RequestHead;
type RequestBody = http::Chunk;
type RequestBody = B::Item;
type Response = http::ResponseHead;
type ResponseBody = http::Chunk;
type Error = ::Error;
type Transport = http::Conn<T, http::ClientTransaction, Pooled<TokioClient>>;
type BindTransport = BindingClient<T>;
type Transport = http::Conn<T, B::Item, http::ClientTransaction, Pooled<TokioClient<B>>>;
type BindTransport = BindingClient<T, B>;
fn bind_transport(&self, io: T) -> Self::BindTransport {
BindingClient {
@@ -207,13 +231,17 @@ impl<T: Io + 'static> ClientProto<T> for HttpClient {
}
}
struct BindingClient<T> {
rx: relay::Receiver<Pooled<TokioClient>>,
struct BindingClient<T, B> {
rx: relay::Receiver<Pooled<TokioClient<B>>>,
io: Option<T>,
}
impl<T: Io + 'static> Future for BindingClient<T> {
type Item = http::Conn<T, http::ClientTransaction, Pooled<TokioClient>>;
impl<T, B> Future for BindingClient<T, B>
where T: Io + 'static,
B: Stream<Error=::Error>,
B::Item: AsRef<[u8]>,
{
type Item = http::Conn<T, B::Item, http::ClientTransaction, Pooled<TokioClient<B>>>;
type Error = io::Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
@@ -228,8 +256,8 @@ impl<T: Io + 'static> Future for BindingClient<T> {
}
/// Configuration for a Client
#[derive(Debug, Clone)]
pub struct Config<C> {
pub struct Config<C, B> {
_body_type: PhantomData<B>,
//connect_timeout: Duration,
connector: C,
keep_alive: bool,
@@ -242,9 +270,10 @@ pub struct Config<C> {
#[derive(Debug, Clone, Copy)]
pub struct UseDefaultConnector(());
impl Config<UseDefaultConnector> {
fn default() -> Config<UseDefaultConnector> {
impl Default for Config<UseDefaultConnector, http::Body> {
fn default() -> Config<UseDefaultConnector, http::Body> {
Config {
_body_type: PhantomData::<http::Body>,
//connect_timeout: Duration::from_secs(10),
connector: UseDefaultConnector(()),
keep_alive: true,
@@ -254,11 +283,33 @@ impl Config<UseDefaultConnector> {
}
}
impl<C> Config<C> {
impl<C, B> Config<C, B> {
/// Set the body stream to be used by the `Client`.
///
/// # Example
///
/// ```rust
/// # use hyper::client::Config;
/// let cfg = Config::default()
/// .body::<hyper::Body>();
/// # drop(cfg);
#[inline]
pub fn body<BB>(self) -> Config<C, BB> {
Config {
_body_type: PhantomData::<BB>,
//connect_timeout: self.connect_timeout,
connector: self.connector,
keep_alive: self.keep_alive,
keep_alive_timeout: self.keep_alive_timeout,
max_idle: self.max_idle,
}
}
/// Set the `Connect` type to be used.
#[inline]
pub fn connector<CC: Connect>(self, val: CC) -> Config<CC> {
pub fn connector<CC: Connect>(self, val: CC) -> Config<CC, B> {
Config {
_body_type: self._body_type,
//connect_timeout: self.connect_timeout,
connector: val,
keep_alive: self.keep_alive,
@@ -271,7 +322,7 @@ impl<C> Config<C> {
///
/// Default is enabled.
#[inline]
pub fn keep_alive(mut self, val: bool) -> Config<C> {
pub fn keep_alive(mut self, val: bool) -> Config<C, B> {
self.keep_alive = val;
self
}
@@ -280,9 +331,9 @@ impl<C> Config<C> {
///
/// Pass `None` to disable timeout.
///
/// Default is 2 minutes.
/// Default is 90 seconds.
#[inline]
pub fn keep_alive_timeout(mut self, val: Option<Duration>) -> Config<C> {
pub fn keep_alive_timeout(mut self, val: Option<Duration>) -> Config<C, B> {
self.keep_alive_timeout = val;
self
}
@@ -292,29 +343,57 @@ impl<C> Config<C> {
///
/// Default is 10 seconds.
#[inline]
pub fn connect_timeout(mut self, val: Duration) -> Config<C> {
pub fn connect_timeout(mut self, val: Duration) -> Config<C, B> {
self.connect_timeout = val;
self
}
*/
}
impl<C: Connect> Config<C> {
impl<C, B> Config<C, B>
where C: Connect,
B: Stream<Error=::Error>,
B::Item: AsRef<[u8]>,
{
/// Construct the Client with this configuration.
#[inline]
pub fn build(self, handle: &Handle) -> Client<C> {
pub fn build(self, handle: &Handle) -> Client<C, B> {
Client::configured(self, handle)
}
}
impl Config<UseDefaultConnector> {
impl<B> Config<UseDefaultConnector, B>
where B: Stream<Error=::Error>,
B::Item: AsRef<[u8]>,
{
/// Construct the Client with this configuration.
#[inline]
pub fn build(self, handle: &Handle) -> Client<HttpConnector> {
pub fn build(self, handle: &Handle) -> Client<HttpConnector, B> {
self.connector(HttpConnector::new(4, handle)).build(handle)
}
}
impl<C, B> fmt::Debug for Config<C, B> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Config")
.field("keep_alive", &self.keep_alive)
.field("keep_alive_timeout", &self.keep_alive_timeout)
.field("max_idle", &self.max_idle)
.finish()
}
}
impl<C: Clone, B> Clone for Config<C, B> {
fn clone(&self) -> Config<C, B> {
Config {
_body_type: PhantomData::<B>,
connector: self.connector.clone(),
keep_alive: self.keep_alive,
keep_alive_timeout: self.keep_alive_timeout,
max_idle: self.max_idle,
}
}
}
#[cfg(test)]
mod tests {

View File

@@ -10,18 +10,18 @@ use version::HttpVersion;
use std::str::FromStr;
/// A client request to a remote server.
pub struct Request {
pub struct Request<B = Body> {
method: Method,
url: Url,
version: HttpVersion,
headers: Headers,
body: Option<Body>,
body: Option<B>,
}
impl Request {
impl<B> Request<B> {
/// Construct a new Request.
#[inline]
pub fn new(method: Method, url: Url) -> Request {
pub fn new(method: Method, url: Url) -> Request<B> {
Request {
method: method,
url: url,
@@ -65,10 +65,10 @@ impl Request {
/// Set the body of the request.
#[inline]
pub fn set_body<T: Into<Body>>(&mut self, body: T) { self.body = Some(body.into()); }
pub fn set_body<T: Into<B>>(&mut self, body: T) { self.body = Some(body.into()); }
}
impl fmt::Debug for Request {
impl<B> fmt::Debug for Request<B> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Request")
.field("method", &self.method)
@@ -79,7 +79,7 @@ impl fmt::Debug for Request {
}
}
pub fn split(req: Request) -> (RequestHead, Option<Body>) {
pub fn split<B>(req: Request<B>) -> (RequestHead, Option<B>) {
let uri = Uri::from_str(&req.url[::url::Position::BeforePath..::url::Position::AfterQuery]).expect("url is uri");
let head = RequestHead {
subject: ::http::RequestLine(req.method, uri),
@@ -198,38 +198,5 @@ mod tests {
assert_eq!(&s[..request_line.len()], request_line);
assert!(s.contains("Host: example.dom"));
}
#[test]
fn test_post_chunked_with_encoding() {
let url = Url::parse("http://example.dom").unwrap();
let mut req = Request::with_connector(
Post, url, &mut MockConnector
).unwrap();
req.headers_mut().set(TransferEncoding(vec![Encoding::Chunked]));
let bytes = run_request(req);
let s = from_utf8(&bytes[..]).unwrap();
assert!(!s.contains("Content-Length:"));
assert!(s.contains("Transfer-Encoding:"));
}
#[test]
fn test_write_error_closes() {
let url = Url::parse("http://hyper.rs").unwrap();
let req = Request::with_connector(
Get, url, &mut MockConnector
).unwrap();
let mut req = req.start().unwrap();
req.message.downcast_mut::<Http11Message>().unwrap()
.get_mut().downcast_mut::<MockStream>().unwrap()
.error_on_write = true;
req.write(b"foo").unwrap();
assert!(req.flush().is_err());
assert!(req.message.downcast_ref::<Http11Message>().unwrap()
.get_ref().downcast_ref::<MockStream>().unwrap()
.is_closed);
}
*/
}