use std::future::Future;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use hyper::rt::{Read, Write};
use tokio::time::timeout;
use hyper::Uri;
use hyper_util::client::legacy::connect::{Connected, Connection};
use tower_service::Service;
mod stream;
use stream::TimeoutStream;
type BoxError = Box<dyn std::error::Error + Send + Sync>;
#[derive(Debug, Clone)]
pub struct TimeoutConnector<T> {
connector: T,
connect_timeout: Option<Duration>,
read_timeout: Option<Duration>,
write_timeout: Option<Duration>,
}
impl<T> TimeoutConnector<T>
where
T: Service<Uri> + Send,
T::Response: Read + Write + Send + Unpin,
T::Future: Send + 'static,
T::Error: Into<BoxError>,
{
pub fn new(connector: T) -> Self {
TimeoutConnector {
connector,
connect_timeout: None,
read_timeout: None,
write_timeout: None,
}
}
}
impl<T> Service<Uri> for TimeoutConnector<T>
where
T: Service<Uri> + Send,
T::Response: Read + Write + Connection + Send + Unpin,
T::Future: Send + 'static,
T::Error: Into<BoxError>,
{
type Response = Pin<Box<TimeoutStream<T::Response>>>;
type Error = BoxError;
#[allow(clippy::type_complexity)]
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.connector.poll_ready(cx).map_err(Into::into)
}
fn call(&mut self, dst: Uri) -> Self::Future {
let connect_timeout = self.connect_timeout;
let read_timeout = self.read_timeout;
let write_timeout = self.write_timeout;
let connecting = self.connector.call(dst);
let fut = async move {
let mut stream = match connect_timeout {
None => {
let io = connecting.await.map_err(Into::into)?;
TimeoutStream::new(io)
}
Some(connect_timeout) => {
let timeout = timeout(connect_timeout, connecting);
let connecting = timeout
.await
.map_err(|e| io::Error::new(io::ErrorKind::TimedOut, e))?;
let io = connecting.map_err(Into::into)?;
TimeoutStream::new(io)
}
};
stream.set_read_timeout(read_timeout);
stream.set_write_timeout(write_timeout);
Ok(Box::pin(stream))
};
Box::pin(fut)
}
}
impl<T> TimeoutConnector<T> {
#[inline]
pub fn set_connect_timeout(&mut self, val: Option<Duration>) {
self.connect_timeout = val;
}
#[inline]
pub fn set_read_timeout(&mut self, val: Option<Duration>) {
self.read_timeout = val;
}
#[inline]
pub fn set_write_timeout(&mut self, val: Option<Duration>) {
self.write_timeout = val;
}
}
impl<T> Connection for TimeoutConnector<T>
where
T: Read + Write + Connection + Service<Uri> + Send + Unpin,
T::Response: Read + Write + Send + Unpin,
T::Future: Send + 'static,
T::Error: Into<BoxError>,
{
fn connected(&self) -> Connected {
self.connector.connected()
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use std::{error::Error, io};
use http_body_util::Empty;
use hyper::body::Bytes;
use hyper_util::{
client::legacy::{connect::HttpConnector, Client},
rt::TokioExecutor,
};
use super::TimeoutConnector;
#[tokio::test]
async fn test_timeout_connector() {
let url = "http://10.255.255.1".parse().unwrap();
let http = HttpConnector::new();
let mut connector = TimeoutConnector::new(http);
connector.set_connect_timeout(Some(Duration::from_millis(1)));
let client = Client::builder(TokioExecutor::new()).build::<_, Empty<Bytes>>(connector);
let res = client.get(url).await;
match res {
Ok(_) => panic!("Expected a timeout"),
Err(e) => {
if let Some(io_e) = e.source().unwrap().downcast_ref::<io::Error>() {
assert_eq!(io_e.kind(), io::ErrorKind::TimedOut);
} else {
panic!("Expected timeout error");
}
}
}
}
#[tokio::test]
async fn test_read_timeout() {
let url = "http://example.com".parse().unwrap();
let http = HttpConnector::new();
let mut connector = TimeoutConnector::new(http);
connector.set_read_timeout(Some(Duration::from_millis(1)));
let client = Client::builder(TokioExecutor::new()).build::<_, Empty<Bytes>>(connector);
let res = client.get(url).await;
if let Err(client_e) = res {
if let Some(hyper_e) = client_e.source() {
if let Some(io_e) = hyper_e.source().unwrap().downcast_ref::<io::Error>() {
return assert_eq!(io_e.kind(), io::ErrorKind::TimedOut);
}
}
}
panic!("Expected timeout error");
}
}