diff --git a/src/error.rs b/src/error.rs index e47dba6..5897834 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,17 +1,108 @@ -use crate::response::Response; -use std::fmt; -use std::io::{self, ErrorKind}; +use url::Url; +use std::error; +use std::fmt::{self, Display}; +use std::io::{self}; + +use crate::Response; + +/// An error that may occur when processing a Request. #[derive(Debug)] -pub enum Error { +pub struct Error { + kind: ErrorKind, + message: Option, + url: Option, + source: Option>, + response: Option>, +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(url) = &self.url { + write!(f, "{}: ", url)?; + } + if let Some(response) = &self.response { + write!(f, "status code {}", response.status())?; + } else { + write!(f, "{:?}", self.kind)?; + } + if let Some(message) = &self.message { + write!(f, ": {}", message)?; + } + if let Some(source) = &self.source { + write!(f, ": {}", source)?; + } + Ok(()) + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + self.source.as_deref() + } +} + +impl Error { + pub(crate) fn new(kind: ErrorKind, message: Option) -> Self { + Error { + kind, + message, + url: None, + source: None, + response: None, + } + } + + pub(crate) fn url(mut self, url: Url) -> Self { + self.url = Some(url); + self + } + + pub(crate) fn src(mut self, e: impl error::Error + 'static) -> Self { + self.source = Some(Box::new(e)); + self + } + + pub(crate) fn response(mut self, response: Response) -> Self { + self.response = Some(Box::new(response)); + self + } + pub(crate) fn kind(&self) -> ErrorKind { + self.kind + } + + /// Return true iff the error was due to a connection closing. + pub(crate) fn connection_closed(&self) -> bool { + if self.kind() != ErrorKind::Io { + return false; + } + let source = match self.source.as_ref() { + Some(e) => e, + None => return false, + }; + let ioe: &Box = match source.downcast_ref() { + Some(e) => e, + None => return false, + }; + match ioe.kind() { + io::ErrorKind::ConnectionAborted => true, + io::ErrorKind::ConnectionReset => true, + _ => false, + } + } +} + +/// One of the types of error the can occur when processing a Request. +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum ErrorKind { /// The url could not be understood. - BadUrl(String), + BadUrl, /// The url scheme could not be understood. - UnknownScheme(String), + UnknownScheme, /// DNS lookup failed. - DnsFailed(String), + DnsFailed, /// Connection to server failed. - ConnectionFailed(String), + ConnectionFailed, /// Too many redirects. TooManyRedirects, /// A status line we don't understand `HTTP/1.1 200 OK`. @@ -19,7 +110,7 @@ pub enum Error { /// A header line that couldn't be parsed. BadHeader, /// Some unspecified `std::io::Error`. - Io(io::Error), + Io, /// Proxy information was not properly formatted BadProxy, /// Proxy credentials were not properly formatted @@ -31,44 +122,60 @@ pub enum Error { /// HTTP status code indicating an error (e.g. 4xx, 5xx) /// Read the inner response body for details and to return /// the connection to the pool. - HTTP(Box), + HTTP, } -impl Error { - // Return true iff the error was due to a connection closing. - pub(crate) fn connection_closed(&self) -> bool { - match self { - Error::Io(e) if e.kind() == ErrorKind::ConnectionAborted => true, - Error::Io(e) if e.kind() == ErrorKind::ConnectionReset => true, - _ => false, - } +impl ErrorKind { + pub(crate) fn new(self) -> Error { + Error::new(self, None) + } + + pub(crate) fn msg(self, s: &str) -> Error { + Error::new(self, Some(s.to_string())) } } impl From for Error { fn from(err: io::Error) -> Error { - Error::Io(err) + ErrorKind::Io.new().src(err) } } -impl fmt::Display for Error { +impl fmt::Display for ErrorKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Error::BadUrl(url) => write!(f, "Bad URL: {}", url), - Error::UnknownScheme(scheme) => write!(f, "Unknown Scheme: {}", scheme), - Error::DnsFailed(err) => write!(f, "Dns Failed: {}", err), - Error::ConnectionFailed(err) => write!(f, "Connection Failed: {}", err), - Error::TooManyRedirects => write!(f, "Too Many Redirects"), - Error::BadStatus => write!(f, "Bad Status"), - Error::BadHeader => write!(f, "Bad Header"), - Error::Io(ioe) => write!(f, "Network Error: {}", ioe), - Error::BadProxy => write!(f, "Malformed proxy"), - Error::BadProxyCreds => write!(f, "Failed to parse proxy credentials"), - Error::ProxyConnect => write!(f, "Proxy failed to connect"), - Error::InvalidProxyCreds => write!(f, "Provided proxy credentials are incorrect"), - Error::HTTP(response) => write!(f, "HTTP status {}", response.status()), + ErrorKind::BadUrl => write!(f, "Bad URL"), + ErrorKind::UnknownScheme => write!(f, "Unknown Scheme"), + ErrorKind::DnsFailed => write!(f, "Dns Failed"), + ErrorKind::ConnectionFailed => write!(f, "Connection Failed"), + ErrorKind::TooManyRedirects => write!(f, "Too Many Redirects"), + ErrorKind::BadStatus => write!(f, "Bad Status"), + ErrorKind::BadHeader => write!(f, "Bad Header"), + ErrorKind::Io => write!(f, "Network Error"), + ErrorKind::BadProxy => write!(f, "Malformed proxy"), + ErrorKind::BadProxyCreds => write!(f, "Failed to parse proxy credentials"), + ErrorKind::ProxyConnect => write!(f, "Proxy failed to connect"), + ErrorKind::InvalidProxyCreds => write!(f, "Provided proxy credentials are incorrect"), + ErrorKind::HTTP => write!(f, "HTTP status error"), } } } -impl std::error::Error for Error {} +#[test] +fn status_code_error() { + let mut err = Error::new(ErrorKind::HTTP, None); + err = err.response(Response::new(500, "Internal Server Error", "too much going on").unwrap()); + assert_eq!(err.to_string(), "status code 500"); + + err = err.url("http://example.com/".parse().unwrap()); + assert_eq!(err.to_string(), "http://example.com/: status code 500"); +} + +#[test] +fn io_error() { + let ioe = io::Error::new(io::ErrorKind::TimedOut, "too slow"); + let mut err = Error::new(ErrorKind::Io, Some("oops".to_string())).src(ioe); + + err = err.url("http://example.com/".parse().unwrap()); + assert_eq!(err.to_string(), "http://example.com/: Io: oops: too slow"); +} diff --git a/src/header.rs b/src/header.rs index 2e6ac3c..5fb5675 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,4 +1,4 @@ -use crate::error::Error; +use crate::error::{Error, ErrorKind}; use std::fmt; use std::str::FromStr; @@ -66,9 +66,10 @@ impl Header { pub(crate) fn validate(&self) -> Result<(), Error> { if !valid_name(self.name()) || !valid_value(&self.line.as_str()[self.index + 1..]) { - return Err(Error::BadHeader); + Err(ErrorKind::BadHeader.msg(&format!("invalid header '{}'", self.line))) + } else { + Ok(()) } - Ok(()) } } @@ -150,11 +151,13 @@ impl FromStr for Header { fn from_str(s: &str) -> Result { // let line = s.to_string(); - let index = s.find(':').ok_or_else(|| Error::BadHeader)?; + let index = s + .find(':') + .ok_or_else(|| ErrorKind::BadHeader.msg("no colon in header"))?; // no value? if index >= s.len() { - return Err(Error::BadHeader); + return Err(ErrorKind::BadHeader.msg("no value in header")); } let header = Header { line, index }; @@ -203,7 +206,7 @@ fn test_parse_invalid_name() { for c in cases { let result = c.parse::
(); assert!( - matches!(result, Err(Error::BadHeader)), + matches!(result, Err(ref e) if e.kind() == ErrorKind::BadHeader), "'{}'.parse(): expected BadHeader, got {:?}", c, result diff --git a/src/lib.rs b/src/lib.rs index e18a7b5..33dc1f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -215,7 +215,7 @@ mod testserver; pub use crate::agent::Agent; pub use crate::agent::AgentBuilder; -pub use crate::error::Error; +pub use crate::error::{Error, ErrorKind}; pub use crate::header::Header; pub use crate::proxy::Proxy; pub use crate::request::Request; @@ -336,6 +336,7 @@ mod tests { #[cfg(feature = "tls")] fn connect_https_invalid_name() { let result = get("https://example.com{REQUEST_URI}/").call(); - assert!(matches!(result.unwrap_err(), Error::DnsFailed(_))); + let e = ErrorKind::DnsFailed; + assert_eq!(result.unwrap_err().kind(), e); } } diff --git a/src/proxy.rs b/src/proxy.rs index 692827e..5f1bad3 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -1,4 +1,4 @@ -use crate::error::Error; +use crate::error::{Error, ErrorKind}; /// Proxy protocol #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -30,7 +30,7 @@ impl Proxy { .into_iter(); if parts.len() != 2 { - Err(Error::BadProxyCreds) + Err(ErrorKind::BadProxyCreds.new()) } else { Ok(( parts.next().map(String::from), @@ -46,14 +46,14 @@ impl Proxy { match host { Some(host) => { let mut parts = host.as_ref().split(':').collect::>().into_iter(); - let host = parts.next().ok_or(Error::BadProxy)?; + let host = parts.next().ok_or(ErrorKind::BadProxy.new())?; let port = parts.next(); Ok(( String::from(host), port.and_then(|port| port.parse::().ok()), )) } - None => Err(Error::BadProxy), + None => Err(ErrorKind::BadProxy.new()), } } @@ -84,7 +84,7 @@ impl Proxy { Some("http") => Proto::HTTPConnect, Some("socks") => Proto::SOCKS5, Some("socks5") => Proto::SOCKS5, - _ => return Err(Error::BadProxy), + _ => return Err(ErrorKind::BadProxy.new()), } } else { Proto::HTTPConnect @@ -92,7 +92,7 @@ impl Proxy { let remaining_parts = proxy_parts.next(); if remaining_parts == None { - return Err(Error::BadProxy); + return Err(ErrorKind::BadProxy.new()); } let mut creds_server_port_parts = remaining_parts @@ -152,13 +152,19 @@ Proxy-Connection: Keep-Alive\r\n\ pub(crate) fn verify_response(response: &[u8]) -> Result<(), Error> { let response_string = String::from_utf8_lossy(response); - let top_line = response_string.lines().next().ok_or(Error::ProxyConnect)?; - let status_code = top_line.split_whitespace().nth(1).ok_or(Error::BadProxy)?; + let top_line = response_string + .lines() + .next() + .ok_or(ErrorKind::ProxyConnect.new())?; + let status_code = top_line + .split_whitespace() + .nth(1) + .ok_or(ErrorKind::BadProxy.new())?; match status_code { "200" => Ok(()), - "401" | "407" => Err(Error::InvalidProxyCreds), - _ => Err(Error::BadProxy), + "401" | "407" => Err(ErrorKind::InvalidProxyCreds.new()), + _ => Err(ErrorKind::BadProxy.new()), } } } diff --git a/src/request.rs b/src/request.rs index d0fe8e6..6e58c01 100644 --- a/src/request.rs +++ b/src/request.rs @@ -3,12 +3,12 @@ use std::io::Read; use url::{form_urlencoded, Url}; -use crate::agent::Agent; use crate::body::Payload; -use crate::error::Error; +use crate::error::ErrorKind; use crate::header::{self, Header}; use crate::unit::{self, Unit}; use crate::Response; +use crate::{agent::Agent, error::Error}; #[cfg(feature = "json")] use super::SerdeValue; @@ -79,19 +79,20 @@ impl Request { for h in &self.headers { h.validate()?; } - let mut url: Url = self - .url - .parse() - .map_err(|e: url::ParseError| Error::BadUrl(e.to_string()))?; + let mut url: Url = self.url.parse().map_err(|e: url::ParseError| { + ErrorKind::BadUrl + .msg(&format!("failed to parse URL '{}'", self.url)) + .src(e) + })?; for (name, value) in self.query_params.clone() { url.query_pairs_mut().append_pair(&name, &value); } let reader = payload.into_read(); let unit = Unit::new(&self.agent, &self.method, &url, &self.headers, &reader); - let response = unit::connect(unit, true, 0, reader, false)?; + let response = unit::connect(unit, true, 0, reader, false).map_err(|e| e.url(url))?; if response.error() && self.error_on_non_2xx { - Err(Error::HTTP(response.into())) + Err(ErrorKind::HTTP.new().response(response)) } else { Ok(response) } diff --git a/src/response.rs b/src/response.rs index 09e00cc..b19e6c5 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,10 +1,10 @@ use std::fmt; -use std::io::{self, Cursor, ErrorKind, Read}; +use std::io::{self, Cursor, Read}; use std::str::FromStr; use chunked_transfer::Decoder as ChunkDecoder; -use crate::error::Error; +use crate::error::{Error, ErrorKind}; use crate::header::Header; use crate::pool::PoolReturnRead; use crate::stream::{DeadlineStream, Stream}; @@ -371,13 +371,13 @@ impl Response { // We make a clone of the original error since serde_json::Error doesn't // let us get the wrapped error instance back. if let Some(ioe) = e.source().and_then(|s| s.downcast_ref::()) { - if ioe.kind() == ErrorKind::TimedOut { + if ioe.kind() == io::ErrorKind::TimedOut { return io_err_timeout(ioe.to_string()); } } io::Error::new( - ErrorKind::InvalidData, + io::ErrorKind::InvalidData, format!("Failed to read JSON: {}", e), ) }) @@ -413,7 +413,7 @@ impl Response { let reader = self.into_reader(); serde_json::from_reader(reader).map_err(|e| { io::Error::new( - ErrorKind::InvalidData, + io::ErrorKind::InvalidData, format!("Failed to read JSON: {}", e), ) }) @@ -473,19 +473,21 @@ fn parse_status_line(line: &str) -> Result<(ResponseStatusIndex, u16), Error> { let mut split = line.splitn(3, ' '); - let http_version = split.next().ok_or_else(|| Error::BadStatus)?; + let http_version = split.next().ok_or_else(|| ErrorKind::BadStatus.new())?; if http_version.len() < 5 { - return Err(Error::BadStatus); + return Err(ErrorKind::BadStatus.new()); } let index1 = http_version.len(); - let status = split.next().ok_or_else(|| Error::BadStatus)?; + let status = split.next().ok_or_else(|| ErrorKind::BadStatus.new())?; if status.len() < 2 { - return Err(Error::BadStatus); + return Err(ErrorKind::BadStatus.new()); } let index2 = index1 + status.len(); - let status = status.parse::().map_err(|_| Error::BadStatus)?; + let status = status + .parse::() + .map_err(|_| ErrorKind::BadStatus.new())?; Ok(( ResponseStatusIndex { @@ -540,7 +542,7 @@ fn read_next_line(reader: &mut R) -> io::Result { if amt == 0 { return Err(io::Error::new( - ErrorKind::ConnectionAborted, + io::ErrorKind::ConnectionAborted, "Unexpected EOF", )); } @@ -549,8 +551,9 @@ fn read_next_line(reader: &mut R) -> io::Result { if byte == b'\n' && prev_byte_was_cr { buf.pop(); // removing the '\r' - return String::from_utf8(buf) - .map_err(|_| io::Error::new(ErrorKind::InvalidInput, "Header is not in ASCII")); + return String::from_utf8(buf).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidInput, "Header is not in ASCII") + }); } prev_byte_was_cr = byte == b'\r'; @@ -594,7 +597,7 @@ impl Read for LimitedRead { // received, the recipient MUST consider the message to be // incomplete and close the connection. Ok(0) => Err(io::Error::new( - ErrorKind::InvalidData, + io::ErrorKind::InvalidData, "response body closed before all bytes were read", )), Ok(amount) => { @@ -743,7 +746,7 @@ mod tests { fn parse_borked_header() { let s = "HTTP/1.1 BORKED\r\n".to_string(); let err = s.parse::().unwrap_err(); - assert!(matches!(err, Error::BadStatus)); + assert_eq!(err.kind(), ErrorKind::BadStatus); } } diff --git a/src/stream.rs b/src/stream.rs index 65d7d9e..b551094 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,6 +1,6 @@ use log::debug; use std::fmt; -use std::io::{self, BufRead, BufReader, Cursor, ErrorKind, Read, Write}; +use std::io::{self, BufRead, BufReader, Cursor, Read, Write}; use std::net::SocketAddr; use std::net::TcpStream; use std::time::Duration; @@ -15,10 +15,10 @@ use rustls::StreamOwned; #[cfg(feature = "socks-proxy")] use socks::{TargetAddr, ToTargetAddr}; -use crate::proxy::Proto; use crate::proxy::Proxy; +use crate::{error::Error, proxy::Proto}; -use crate::error::Error; +use crate::error::ErrorKind; use crate::unit::Unit; #[allow(clippy::large_enum_variant)] @@ -67,7 +67,7 @@ impl Read for DeadlineStream { // causes ErrorKind::WouldBlock instead of ErrorKind::TimedOut. // Since the socket most definitely not set_nonblocking(true), // we can safely normalize WouldBlock to TimedOut - if e.kind() == ErrorKind::WouldBlock { + if e.kind() == io::ErrorKind::WouldBlock { return io_err_timeout("timed out reading response".to_string()); } e @@ -86,7 +86,7 @@ fn time_until_deadline(deadline: Instant) -> io::Result { } pub(crate) fn io_err_timeout(error: String) -> io::Error { - io::Error::new(ErrorKind::TimedOut, error) + io::Error::new(io::ErrorKind::TimedOut, error) } impl fmt::Debug for Stream { @@ -119,7 +119,7 @@ impl Stream { let result = match stream.peek(&mut buf) { Ok(0) => Ok(true), Ok(_) => Ok(false), // TODO: Maybe this should produce an "unexpected response" error - Err(e) if e.kind() == ErrorKind::WouldBlock => Ok(false), + Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(false), Err(e) => Err(e), }; stream.set_nonblocking(false)?; @@ -241,7 +241,7 @@ fn read_https( #[allow(deprecated)] #[cfg(feature = "tls")] fn is_close_notify(e: &std::io::Error) -> bool { - if e.kind() != ErrorKind::ConnectionAborted { + if e.kind() != io::ErrorKind::ConnectionAborted { return false; } @@ -313,7 +313,7 @@ pub(crate) fn connect_https(unit: &Unit, hostname: &str) -> Result = unit .agent .config @@ -347,10 +347,10 @@ pub(crate) fn connect_host(unit: &Unit, hostname: &str, port: u16) -> Result Result = unit .resolver() .resolve(&format!("{}:{}", hostname, port)) - .map_err(|e| std::io::Error::new(ErrorKind::NotFound, format!("DNS failure: {}.", e)))?; + .map_err(|e| { + std::io::Error::new(io::ErrorKind::NotFound, format!("DNS failure: {}.", e)) + })?; if addrs.is_empty() { return Err(std::io::Error::new( - ErrorKind::NotFound, + io::ErrorKind::NotFound, "DNS failure: no socket addrs found.", )); } @@ -458,7 +461,7 @@ fn socks5_local_nslookup( Ok(addr) => Ok(addr), Err(err) => { return Err(std::io::Error::new( - ErrorKind::NotFound, + io::ErrorKind::NotFound, format!("DNS failure: {}.", err), )) } @@ -579,7 +582,7 @@ fn connect_socks5( _port: u16, ) -> Result { Err(std::io::Error::new( - ErrorKind::Other, + io::ErrorKind::Other, "SOCKS5 feature disabled.", )) } @@ -592,10 +595,12 @@ pub(crate) fn connect_test(unit: &Unit) -> Result { #[cfg(not(test))] pub(crate) fn connect_test(unit: &Unit) -> Result { - Err(Error::UnknownScheme(unit.url.scheme().to_string())) + Err(ErrorKind::UnknownScheme.msg(&format!("unknown scheme '{}'", unit.url.scheme()))) } #[cfg(not(feature = "tls"))] pub(crate) fn connect_https(unit: &Unit, _hostname: &str) -> Result { - Err(Error::UnknownScheme(unit.url.scheme().to_string())) + Err(ErrorKind::UnknownScheme + .msg("URL has 'https:' scheme but ureq was build without HTTP support") + .url(unit.url.clone())) } diff --git a/src/test/agent_test.rs b/src/test/agent_test.rs index 2a47963..36edeaa 100644 --- a/src/test/agent_test.rs +++ b/src/test/agent_test.rs @@ -1,5 +1,6 @@ #![allow(dead_code)] +use crate::error::Error; use crate::testserver::{read_request, TestServer}; use std::io::{self, Read, Write}; use std::net::TcpStream; diff --git a/src/test/mod.rs b/src/test/mod.rs index a26c761..09747cf 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1,10 +1,10 @@ +use crate::error::Error; +use crate::stream::Stream; use crate::unit::Unit; -use crate::{error::Error}; -use crate::{stream::Stream}; use once_cell::sync::Lazy; +use std::collections::HashMap; use std::io::{Cursor, Write}; use std::sync::{Arc, Mutex}; -use std::{collections::HashMap}; mod agent_test; mod body_read; diff --git a/src/test/redirect.rs b/src/test/redirect.rs index e68c70f..d441db0 100644 --- a/src/test/redirect.rs +++ b/src/test/redirect.rs @@ -4,7 +4,7 @@ use std::{ }; use testserver::{self, TestServer}; -use crate::test; +use crate::{error::Error, test}; use super::super::*; @@ -34,7 +34,7 @@ fn redirect_many() { .build() .get("test://host/redirect_many1") .call(); - assert!(matches!(result, Err(Error::TooManyRedirects))); + assert!(matches!(result, Err(e) if e.kind() == ErrorKind::TooManyRedirects)); } #[test] @@ -104,12 +104,11 @@ fn redirect_host() { Ok(()) }); let url = format!("http://localhost:{}/", srv.port); - let resp = crate::Agent::new().get(&url).call(); - let err = resp.err(); + let result = crate::Agent::new().get(&url).call(); assert!( - matches!(err, Some(Error::DnsFailed(_))), - "expected DnsFailed, got: {:?}", - err + matches!(result, Err(ref e) if e.kind() == ErrorKind::DnsFailed), + "expected Err(DnsFailed), got: {:?}", + result ); } diff --git a/src/test/simple.rs b/src/test/simple.rs index 498c672..2c4a66e 100644 --- a/src/test/simple.rs +++ b/src/test/simple.rs @@ -157,13 +157,13 @@ fn non_ascii_header() { test::set_handler("/non_ascii_header", |_unit| { test::make_response(200, "OK", vec!["Wörse: Hädör"], vec![]) }); - let resp = get("test://host/non_ascii_header") + let result = get("test://host/non_ascii_header") .set("Bäd", "Headör") .call(); assert!( - matches!(resp, Err(Error::BadHeader)), - "expected Some(&BadHeader), got {:?}", - resp + matches!(result, Err(ref e) if e.kind() == ErrorKind::BadHeader), + "expected Err(BadHeader), got {:?}", + result ); } diff --git a/src/test/timeout.rs b/src/test/timeout.rs index 358e4ec..c85dafe 100644 --- a/src/test/timeout.rs +++ b/src/test/timeout.rs @@ -1,8 +1,11 @@ use crate::testserver::*; -use std::io::{self, Write}; use std::net::TcpStream; use std::thread; use std::time::Duration; +use std::{ + error::Error, + io::{self, Write}, +}; use super::super::*; @@ -96,10 +99,16 @@ fn read_timeout_during_headers() { let server = TestServer::new(dribble_headers_respond); let url = format!("http://localhost:{}/", server.port); let agent = builder().timeout_read(Duration::from_millis(10)).build(); - let resp = agent.get(&url).call(); - match resp { + let result = agent.get(&url).call(); + match result { Ok(_) => Err("successful response".to_string()), - Err(Error::Io(e)) if e.kind() == io::ErrorKind::TimedOut => Ok(()), + Err(e) if e.kind() == ErrorKind::Io => { + let ioe: Option<&io::Error> = e.source().and_then(|s| s.downcast_ref()); + match ioe { + Some(e) if e.kind() == io::ErrorKind::TimedOut => Ok(()), + _ => Err(format!("wrong error type {:?}", e)), + } + } Err(e) => Err(format!("Unexpected error type: {:?}", e)), } .expect("expected timeout but got something else"); @@ -111,10 +120,16 @@ fn overall_timeout_during_headers() { let server = TestServer::new(dribble_headers_respond); let url = format!("http://localhost:{}/", server.port); let agent = builder().timeout(Duration::from_millis(500)).build(); - let resp = agent.get(&url).call(); - match resp { + let result = agent.get(&url).call(); + match result { Ok(_) => Err("successful response".to_string()), - Err(Error::Io(e)) if e.kind() == io::ErrorKind::TimedOut => Ok(()), + Err(e) if e.kind() == ErrorKind::Io => { + let ioe: Option<&io::Error> = e.source().and_then(|s| s.downcast_ref()); + match ioe { + Some(e) if e.kind() == io::ErrorKind::TimedOut => Ok(()), + _ => Err(format!("wrong error type {:?}", e)), + } + } Err(e) => Err(format!("Unexpected error type: {:?}", e)), } .expect("expected timeout but got something else"); diff --git a/src/unit.rs b/src/unit.rs index ae6c9d5..51784d6 100644 --- a/src/unit.rs +++ b/src/unit.rs @@ -7,6 +7,7 @@ use url::Url; #[cfg(feature = "cookies")] use cookie::Cookie; +use crate::error::{Error, ErrorKind}; use crate::header; use crate::resolve::ArcResolver; use crate::stream::{self, connect_test, Stream}; @@ -15,7 +16,7 @@ use crate::{ body::{self, BodySize, Payload, SizedReader}, header::get_header, }; -use crate::{Error, Header, Response}; +use crate::{Header, Response}; /// A Unit is fully-built Request, ready to execute. /// @@ -173,7 +174,7 @@ pub(crate) fn connect( let host = unit .url .host_str() - .ok_or(Error::BadUrl("no host".to_string()))?; + .ok_or(ErrorKind::BadUrl.msg("no host in URL"))?; let url = &unit.url; let method = &unit.method; // open socket @@ -234,16 +235,18 @@ pub(crate) fn connect( // handle redirects if resp.redirect() && unit.agent.config.redirects > 0 { if redirect_count == unit.agent.config.redirects { - return Err(Error::TooManyRedirects); + return Err(ErrorKind::TooManyRedirects.new()); } // the location header let location = resp.header("location"); if let Some(location) = location { // join location header to current url in case it it relative - let new_url = url - .join(location) - .map_err(|_| Error::BadUrl(format!("Bad redirection: {}", location)))?; + let new_url = url.join(location).map_err(|e| { + ErrorKind::BadUrl + .msg(&format!("Bad redirection: {}", location)) + .src(e) + })?; // perform the redirect differently depending on 3xx code. match resp.status() { @@ -302,7 +305,7 @@ fn extract_cookies(agent: &Agent, url: &Url) -> Option
{ fn connect_socket(unit: &Unit, hostname: &str, use_pooled: bool) -> Result<(Stream, bool), Error> { match unit.url.scheme() { "http" | "https" | "test" => (), - _ => return Err(Error::UnknownScheme(unit.url.scheme().to_string())), + scheme => return Err(ErrorKind::UnknownScheme.msg(&format!("unknown scheme '{}'", scheme))), }; if use_pooled { let agent = &unit.agent; @@ -324,7 +327,7 @@ fn connect_socket(unit: &Unit, hostname: &str, use_pooled: bool) -> Result<(Stre "http" => stream::connect_http(&unit, hostname), "https" => stream::connect_https(&unit, hostname), "test" => connect_test(&unit), - _ => Err(Error::UnknownScheme(unit.url.scheme().to_string())), + scheme => Err(ErrorKind::UnknownScheme.msg(&format!("unknown scheme {}", scheme))), }; Ok((stream?, false)) }