The reason the original code didn't work was because the reader side provided the response before the client sent the request, so tokio-proto errored out with request / response mismatch . The fix turns out to be non trivial in that first we need to arrange for the reader to block, or more specifically error out with std::io::ErrorKind::WouldBlock to indicate to the event loop that there isn't anything yet, but don't consider it an EOF . Additionally once we get the write which indicates the request has been sent and the tokio-proto machinery is waiting for a response, we use futures::task::current.notify to unblock the read. Here's an updated implementation that works as expected:

extern crate futures; extern crate hyper; extern crate tokio_core; extern crate tokio_io; use futures::{future, Future, Stream, task, Poll}; use std::str::from_utf8; use std::io::{self, Cursor, Read, Write}; use tokio_io::{AsyncRead, AsyncWrite}; struct Client<'a, C: 'a> { client: &'a hyper::Client<C>, url: &'a str, } impl<'a, C: hyper::client::Connect> Client<'a, C> { fn get(&self) -> Box<Future<Item = String, Error = hyper::Error>> { Box::new(self.client.get(self.url.parse().unwrap()).and_then(|res| { let body = Vec::new(); res.body() .fold(body, |mut acc, chunk| { acc.extend_from_slice(chunk.as_ref()); Ok::<_, hyper::Error>(acc) }) .and_then(move |value| Ok(String::from(from_utf8(&value).unwrap()))) })) } } struct StaticStream { wrote: bool, body: Cursor<Vec<u8>>, } impl Read for StaticStream { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { if self.wrote { self.body.read(buf) } else { Err(io::ErrorKind::WouldBlock.into()) } } } impl Write for StaticStream { fn write<'a>(&mut self, buf: &'a [u8]) -> io::Result<usize> { self.wrote = true; task::current().notify(); Ok(buf.len()) } fn flush(&mut self) -> io::Result<()> { Ok(()) } } impl AsyncRead for StaticStream {} impl AsyncWrite for StaticStream { fn shutdown(&mut self) -> Poll<(), io::Error> { Ok(().into()) } } struct StaticConnector<'a> { body: &'a [u8], } impl<'a> StaticConnector<'a> { fn new(body: &'a [u8]) -> StaticConnector { StaticConnector { body: body } } } impl<'a> hyper::server::Service for StaticConnector<'a> { type Request = hyper::Uri; type Response = StaticStream; type Error = std::io::Error; type Future = Box<Future<Item = Self::Response, Error = Self::Error>>; fn call(&self, _: Self::Request) -> Self::Future { Box::new(future::ok(StaticStream { wrote: false, body: Cursor::new(self.body.to_vec()), })) } } fn main() { let mut core = tokio_core::reactor::Core::new().unwrap(); let handle = core.handle(); // My StaticConnector for testing let hyper_client = hyper::Client::configure() .connector(StaticConnector::new( b"\ HTTP/1.1 200 OK\r

\ Content-Length: 8\r

\ \r

\ Maldives\ ", )) .build(&handle); // Real Connector /* let hyper_client = hyper::Client::configure().build(&handle); */ let client = Client { client: &hyper_client, url: "http://ifconfig.co/country", }; let result = core.run(client.get()).unwrap(); println!("{}", result); }

Playground

Note: This implementation works for simple cases, but I haven't tested more complex scenarios. For example one thing I'm unsure of is how large request/responses behave as they may involve more than 1 read/write call.