Rewrite the Error type. (#234)

This adds a source field to keep track of upstream errors and allow
backtraces, plus a URL field to indicate what URL an error was
associated with.

The enum variants we used to use for Error are now part of a new
ErrorKind type. For convenience within ureq, ErrorKinds can be turned
into an Error with `.new()` or `.msg("some additional information")`.

Error acts as a builder, so additional information can be added after
initial construction. For instance, we return a DnsFailed error when
name resolution fails. When that error bubbles up to Request's
`do_call`, Request adds the URL.

Fixes #232.
This commit is contained in:
Jacob Hoffman-Andrews
2020-11-21 16:14:44 -08:00
committed by GitHub
parent dac517e30e
commit fade03b54e
13 changed files with 266 additions and 122 deletions

View File

@@ -1,17 +1,108 @@
use crate::response::Response; use url::Url;
use std::fmt;
use std::io::{self, ErrorKind};
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)] #[derive(Debug)]
pub enum Error { pub struct Error {
kind: ErrorKind,
message: Option<String>,
url: Option<Url>,
source: Option<Box<dyn error::Error>>,
response: Option<Box<Response>>,
}
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<String>) -> 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<io::Error> = 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. /// The url could not be understood.
BadUrl(String), BadUrl,
/// The url scheme could not be understood. /// The url scheme could not be understood.
UnknownScheme(String), UnknownScheme,
/// DNS lookup failed. /// DNS lookup failed.
DnsFailed(String), DnsFailed,
/// Connection to server failed. /// Connection to server failed.
ConnectionFailed(String), ConnectionFailed,
/// Too many redirects. /// Too many redirects.
TooManyRedirects, TooManyRedirects,
/// A status line we don't understand `HTTP/1.1 200 OK`. /// 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. /// A header line that couldn't be parsed.
BadHeader, BadHeader,
/// Some unspecified `std::io::Error`. /// Some unspecified `std::io::Error`.
Io(io::Error), Io,
/// Proxy information was not properly formatted /// Proxy information was not properly formatted
BadProxy, BadProxy,
/// Proxy credentials were not properly formatted /// Proxy credentials were not properly formatted
@@ -31,44 +122,60 @@ pub enum Error {
/// HTTP status code indicating an error (e.g. 4xx, 5xx) /// HTTP status code indicating an error (e.g. 4xx, 5xx)
/// Read the inner response body for details and to return /// Read the inner response body for details and to return
/// the connection to the pool. /// the connection to the pool.
HTTP(Box<Response>), HTTP,
} }
impl Error { impl ErrorKind {
// Return true iff the error was due to a connection closing. pub(crate) fn new(self) -> Error {
pub(crate) fn connection_closed(&self) -> bool { Error::new(self, None)
match self {
Error::Io(e) if e.kind() == ErrorKind::ConnectionAborted => true,
Error::Io(e) if e.kind() == ErrorKind::ConnectionReset => true,
_ => false,
} }
pub(crate) fn msg(self, s: &str) -> Error {
Error::new(self, Some(s.to_string()))
} }
} }
impl From<io::Error> for Error { impl From<io::Error> for Error {
fn from(err: io::Error) -> 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 { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
Error::BadUrl(url) => write!(f, "Bad URL: {}", url), ErrorKind::BadUrl => write!(f, "Bad URL"),
Error::UnknownScheme(scheme) => write!(f, "Unknown Scheme: {}", scheme), ErrorKind::UnknownScheme => write!(f, "Unknown Scheme"),
Error::DnsFailed(err) => write!(f, "Dns Failed: {}", err), ErrorKind::DnsFailed => write!(f, "Dns Failed"),
Error::ConnectionFailed(err) => write!(f, "Connection Failed: {}", err), ErrorKind::ConnectionFailed => write!(f, "Connection Failed"),
Error::TooManyRedirects => write!(f, "Too Many Redirects"), ErrorKind::TooManyRedirects => write!(f, "Too Many Redirects"),
Error::BadStatus => write!(f, "Bad Status"), ErrorKind::BadStatus => write!(f, "Bad Status"),
Error::BadHeader => write!(f, "Bad Header"), ErrorKind::BadHeader => write!(f, "Bad Header"),
Error::Io(ioe) => write!(f, "Network Error: {}", ioe), ErrorKind::Io => write!(f, "Network Error"),
Error::BadProxy => write!(f, "Malformed proxy"), ErrorKind::BadProxy => write!(f, "Malformed proxy"),
Error::BadProxyCreds => write!(f, "Failed to parse proxy credentials"), ErrorKind::BadProxyCreds => write!(f, "Failed to parse proxy credentials"),
Error::ProxyConnect => write!(f, "Proxy failed to connect"), ErrorKind::ProxyConnect => write!(f, "Proxy failed to connect"),
Error::InvalidProxyCreds => write!(f, "Provided proxy credentials are incorrect"), ErrorKind::InvalidProxyCreds => write!(f, "Provided proxy credentials are incorrect"),
Error::HTTP(response) => write!(f, "HTTP status {}", response.status()), 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");
}

View File

@@ -1,4 +1,4 @@
use crate::error::Error; use crate::error::{Error, ErrorKind};
use std::fmt; use std::fmt;
use std::str::FromStr; use std::str::FromStr;
@@ -66,10 +66,11 @@ impl Header {
pub(crate) fn validate(&self) -> Result<(), Error> { pub(crate) fn validate(&self) -> Result<(), Error> {
if !valid_name(self.name()) || !valid_value(&self.line.as_str()[self.index + 1..]) { 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(())
} }
}
} }
pub fn get_header<'a, 'b>(headers: &'b [Header], name: &'a str) -> Option<&'b str> { pub fn get_header<'a, 'b>(headers: &'b [Header], name: &'a str) -> Option<&'b str> {
@@ -150,11 +151,13 @@ impl FromStr for Header {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
// //
let line = s.to_string(); 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? // no value?
if index >= s.len() { if index >= s.len() {
return Err(Error::BadHeader); return Err(ErrorKind::BadHeader.msg("no value in header"));
} }
let header = Header { line, index }; let header = Header { line, index };
@@ -203,7 +206,7 @@ fn test_parse_invalid_name() {
for c in cases { for c in cases {
let result = c.parse::<Header>(); let result = c.parse::<Header>();
assert!( assert!(
matches!(result, Err(Error::BadHeader)), matches!(result, Err(ref e) if e.kind() == ErrorKind::BadHeader),
"'{}'.parse(): expected BadHeader, got {:?}", "'{}'.parse(): expected BadHeader, got {:?}",
c, c,
result result

View File

@@ -215,7 +215,7 @@ mod testserver;
pub use crate::agent::Agent; pub use crate::agent::Agent;
pub use crate::agent::AgentBuilder; pub use crate::agent::AgentBuilder;
pub use crate::error::Error; pub use crate::error::{Error, ErrorKind};
pub use crate::header::Header; pub use crate::header::Header;
pub use crate::proxy::Proxy; pub use crate::proxy::Proxy;
pub use crate::request::Request; pub use crate::request::Request;
@@ -336,6 +336,7 @@ mod tests {
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
fn connect_https_invalid_name() { fn connect_https_invalid_name() {
let result = get("https://example.com{REQUEST_URI}/").call(); 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);
} }
} }

View File

@@ -1,4 +1,4 @@
use crate::error::Error; use crate::error::{Error, ErrorKind};
/// Proxy protocol /// Proxy protocol
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -30,7 +30,7 @@ impl Proxy {
.into_iter(); .into_iter();
if parts.len() != 2 { if parts.len() != 2 {
Err(Error::BadProxyCreds) Err(ErrorKind::BadProxyCreds.new())
} else { } else {
Ok(( Ok((
parts.next().map(String::from), parts.next().map(String::from),
@@ -46,14 +46,14 @@ impl Proxy {
match host { match host {
Some(host) => { Some(host) => {
let mut parts = host.as_ref().split(':').collect::<Vec<&str>>().into_iter(); let mut parts = host.as_ref().split(':').collect::<Vec<&str>>().into_iter();
let host = parts.next().ok_or(Error::BadProxy)?; let host = parts.next().ok_or(ErrorKind::BadProxy.new())?;
let port = parts.next(); let port = parts.next();
Ok(( Ok((
String::from(host), String::from(host),
port.and_then(|port| port.parse::<u32>().ok()), port.and_then(|port| port.parse::<u32>().ok()),
)) ))
} }
None => Err(Error::BadProxy), None => Err(ErrorKind::BadProxy.new()),
} }
} }
@@ -84,7 +84,7 @@ impl Proxy {
Some("http") => Proto::HTTPConnect, Some("http") => Proto::HTTPConnect,
Some("socks") => Proto::SOCKS5, Some("socks") => Proto::SOCKS5,
Some("socks5") => Proto::SOCKS5, Some("socks5") => Proto::SOCKS5,
_ => return Err(Error::BadProxy), _ => return Err(ErrorKind::BadProxy.new()),
} }
} else { } else {
Proto::HTTPConnect Proto::HTTPConnect
@@ -92,7 +92,7 @@ impl Proxy {
let remaining_parts = proxy_parts.next(); let remaining_parts = proxy_parts.next();
if remaining_parts == None { if remaining_parts == None {
return Err(Error::BadProxy); return Err(ErrorKind::BadProxy.new());
} }
let mut creds_server_port_parts = remaining_parts 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> { pub(crate) fn verify_response(response: &[u8]) -> Result<(), Error> {
let response_string = String::from_utf8_lossy(response); let response_string = String::from_utf8_lossy(response);
let top_line = response_string.lines().next().ok_or(Error::ProxyConnect)?; let top_line = response_string
let status_code = top_line.split_whitespace().nth(1).ok_or(Error::BadProxy)?; .lines()
.next()
.ok_or(ErrorKind::ProxyConnect.new())?;
let status_code = top_line
.split_whitespace()
.nth(1)
.ok_or(ErrorKind::BadProxy.new())?;
match status_code { match status_code {
"200" => Ok(()), "200" => Ok(()),
"401" | "407" => Err(Error::InvalidProxyCreds), "401" | "407" => Err(ErrorKind::InvalidProxyCreds.new()),
_ => Err(Error::BadProxy), _ => Err(ErrorKind::BadProxy.new()),
} }
} }
} }

View File

@@ -3,12 +3,12 @@ use std::io::Read;
use url::{form_urlencoded, Url}; use url::{form_urlencoded, Url};
use crate::agent::Agent;
use crate::body::Payload; use crate::body::Payload;
use crate::error::Error; use crate::error::ErrorKind;
use crate::header::{self, Header}; use crate::header::{self, Header};
use crate::unit::{self, Unit}; use crate::unit::{self, Unit};
use crate::Response; use crate::Response;
use crate::{agent::Agent, error::Error};
#[cfg(feature = "json")] #[cfg(feature = "json")]
use super::SerdeValue; use super::SerdeValue;
@@ -79,19 +79,20 @@ impl Request {
for h in &self.headers { for h in &self.headers {
h.validate()?; h.validate()?;
} }
let mut url: Url = self let mut url: Url = self.url.parse().map_err(|e: url::ParseError| {
.url ErrorKind::BadUrl
.parse() .msg(&format!("failed to parse URL '{}'", self.url))
.map_err(|e: url::ParseError| Error::BadUrl(e.to_string()))?; .src(e)
})?;
for (name, value) in self.query_params.clone() { for (name, value) in self.query_params.clone() {
url.query_pairs_mut().append_pair(&name, &value); url.query_pairs_mut().append_pair(&name, &value);
} }
let reader = payload.into_read(); let reader = payload.into_read();
let unit = Unit::new(&self.agent, &self.method, &url, &self.headers, &reader); 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 { if response.error() && self.error_on_non_2xx {
Err(Error::HTTP(response.into())) Err(ErrorKind::HTTP.new().response(response))
} else { } else {
Ok(response) Ok(response)
} }

View File

@@ -1,10 +1,10 @@
use std::fmt; use std::fmt;
use std::io::{self, Cursor, ErrorKind, Read}; use std::io::{self, Cursor, Read};
use std::str::FromStr; use std::str::FromStr;
use chunked_transfer::Decoder as ChunkDecoder; use chunked_transfer::Decoder as ChunkDecoder;
use crate::error::Error; use crate::error::{Error, ErrorKind};
use crate::header::Header; use crate::header::Header;
use crate::pool::PoolReturnRead; use crate::pool::PoolReturnRead;
use crate::stream::{DeadlineStream, Stream}; 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 // We make a clone of the original error since serde_json::Error doesn't
// let us get the wrapped error instance back. // let us get the wrapped error instance back.
if let Some(ioe) = e.source().and_then(|s| s.downcast_ref::<io::Error>()) { if let Some(ioe) = e.source().and_then(|s| s.downcast_ref::<io::Error>()) {
if ioe.kind() == ErrorKind::TimedOut { if ioe.kind() == io::ErrorKind::TimedOut {
return io_err_timeout(ioe.to_string()); return io_err_timeout(ioe.to_string());
} }
} }
io::Error::new( io::Error::new(
ErrorKind::InvalidData, io::ErrorKind::InvalidData,
format!("Failed to read JSON: {}", e), format!("Failed to read JSON: {}", e),
) )
}) })
@@ -413,7 +413,7 @@ impl Response {
let reader = self.into_reader(); let reader = self.into_reader();
serde_json::from_reader(reader).map_err(|e| { serde_json::from_reader(reader).map_err(|e| {
io::Error::new( io::Error::new(
ErrorKind::InvalidData, io::ErrorKind::InvalidData,
format!("Failed to read JSON: {}", e), 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 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 { if http_version.len() < 5 {
return Err(Error::BadStatus); return Err(ErrorKind::BadStatus.new());
} }
let index1 = http_version.len(); 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 { if status.len() < 2 {
return Err(Error::BadStatus); return Err(ErrorKind::BadStatus.new());
} }
let index2 = index1 + status.len(); let index2 = index1 + status.len();
let status = status.parse::<u16>().map_err(|_| Error::BadStatus)?; let status = status
.parse::<u16>()
.map_err(|_| ErrorKind::BadStatus.new())?;
Ok(( Ok((
ResponseStatusIndex { ResponseStatusIndex {
@@ -540,7 +542,7 @@ fn read_next_line<R: Read>(reader: &mut R) -> io::Result<String> {
if amt == 0 { if amt == 0 {
return Err(io::Error::new( return Err(io::Error::new(
ErrorKind::ConnectionAborted, io::ErrorKind::ConnectionAborted,
"Unexpected EOF", "Unexpected EOF",
)); ));
} }
@@ -549,8 +551,9 @@ fn read_next_line<R: Read>(reader: &mut R) -> io::Result<String> {
if byte == b'\n' && prev_byte_was_cr { if byte == b'\n' && prev_byte_was_cr {
buf.pop(); // removing the '\r' buf.pop(); // removing the '\r'
return String::from_utf8(buf) return String::from_utf8(buf).map_err(|_| {
.map_err(|_| io::Error::new(ErrorKind::InvalidInput, "Header is not in ASCII")); io::Error::new(io::ErrorKind::InvalidInput, "Header is not in ASCII")
});
} }
prev_byte_was_cr = byte == b'\r'; prev_byte_was_cr = byte == b'\r';
@@ -594,7 +597,7 @@ impl<R: Read> Read for LimitedRead<R> {
// received, the recipient MUST consider the message to be // received, the recipient MUST consider the message to be
// incomplete and close the connection. // incomplete and close the connection.
Ok(0) => Err(io::Error::new( Ok(0) => Err(io::Error::new(
ErrorKind::InvalidData, io::ErrorKind::InvalidData,
"response body closed before all bytes were read", "response body closed before all bytes were read",
)), )),
Ok(amount) => { Ok(amount) => {
@@ -743,7 +746,7 @@ mod tests {
fn parse_borked_header() { fn parse_borked_header() {
let s = "HTTP/1.1 BORKED\r\n".to_string(); let s = "HTTP/1.1 BORKED\r\n".to_string();
let err = s.parse::<Response>().unwrap_err(); let err = s.parse::<Response>().unwrap_err();
assert!(matches!(err, Error::BadStatus)); assert_eq!(err.kind(), ErrorKind::BadStatus);
} }
} }

View File

@@ -1,6 +1,6 @@
use log::debug; use log::debug;
use std::fmt; 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::SocketAddr;
use std::net::TcpStream; use std::net::TcpStream;
use std::time::Duration; use std::time::Duration;
@@ -15,10 +15,10 @@ use rustls::StreamOwned;
#[cfg(feature = "socks-proxy")] #[cfg(feature = "socks-proxy")]
use socks::{TargetAddr, ToTargetAddr}; use socks::{TargetAddr, ToTargetAddr};
use crate::proxy::Proto;
use crate::proxy::Proxy; use crate::proxy::Proxy;
use crate::{error::Error, proxy::Proto};
use crate::error::Error; use crate::error::ErrorKind;
use crate::unit::Unit; use crate::unit::Unit;
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
@@ -67,7 +67,7 @@ impl Read for DeadlineStream {
// causes ErrorKind::WouldBlock instead of ErrorKind::TimedOut. // causes ErrorKind::WouldBlock instead of ErrorKind::TimedOut.
// Since the socket most definitely not set_nonblocking(true), // Since the socket most definitely not set_nonblocking(true),
// we can safely normalize WouldBlock to TimedOut // 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()); return io_err_timeout("timed out reading response".to_string());
} }
e e
@@ -86,7 +86,7 @@ fn time_until_deadline(deadline: Instant) -> io::Result<Duration> {
} }
pub(crate) fn io_err_timeout(error: String) -> io::Error { 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 { impl fmt::Debug for Stream {
@@ -119,7 +119,7 @@ impl Stream {
let result = match stream.peek(&mut buf) { let result = match stream.peek(&mut buf) {
Ok(0) => Ok(true), Ok(0) => Ok(true),
Ok(_) => Ok(false), // TODO: Maybe this should produce an "unexpected response" error 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), Err(e) => Err(e),
}; };
stream.set_nonblocking(false)?; stream.set_nonblocking(false)?;
@@ -241,7 +241,7 @@ fn read_https(
#[allow(deprecated)] #[allow(deprecated)]
#[cfg(feature = "tls")] #[cfg(feature = "tls")]
fn is_close_notify(e: &std::io::Error) -> bool { fn is_close_notify(e: &std::io::Error) -> bool {
if e.kind() != ErrorKind::ConnectionAborted { if e.kind() != io::ErrorKind::ConnectionAborted {
return false; return false;
} }
@@ -313,7 +313,7 @@ pub(crate) fn connect_https(unit: &Unit, hostname: &str) -> Result<Stream, Error
let port = unit.url.port().unwrap_or(443); let port = unit.url.port().unwrap_or(443);
let sni = webpki::DNSNameRef::try_from_ascii_str(hostname) let sni = webpki::DNSNameRef::try_from_ascii_str(hostname)
.map_err(|err| Error::DnsFailed(err.to_string()))?; .map_err(|err| ErrorKind::DnsFailed.new().src(err))?;
let tls_conf: &Arc<rustls::ClientConfig> = unit let tls_conf: &Arc<rustls::ClientConfig> = unit
.agent .agent
.config .config
@@ -347,10 +347,10 @@ pub(crate) fn connect_host(unit: &Unit, hostname: &str, port: u16) -> Result<Tcp
let sock_addrs = unit let sock_addrs = unit
.resolver() .resolver()
.resolve(&netloc) .resolve(&netloc)
.map_err(|e| Error::DnsFailed(format!("{}", e)))?; .map_err(|e| ErrorKind::DnsFailed.new().src(e))?;
if sock_addrs.is_empty() { if sock_addrs.is_empty() {
return Err(Error::DnsFailed(format!("No ip address for {}", hostname))); return Err(ErrorKind::DnsFailed.msg(&format!("No ip address for {}", hostname)));
} }
let proto = if let Some(ref proxy) = proxy { let proto = if let Some(ref proxy) = proxy {
@@ -396,9 +396,10 @@ pub(crate) fn connect_host(unit: &Unit, hostname: &str, port: u16) -> Result<Tcp
let mut stream = if let Some(stream) = any_stream { let mut stream = if let Some(stream) = any_stream {
stream stream
} else if let Some(e) = any_err {
return Err(ErrorKind::ConnectionFailed.msg("Connect error").src(e));
} else { } else {
let err = Error::ConnectionFailed(format!("{}", any_err.expect("Connect error"))); panic!("shouldn't happen: failed to connect to all IPs, but no error");
return Err(err);
}; };
if let Some(deadline) = unit.deadline { if let Some(deadline) = unit.deadline {
@@ -445,11 +446,13 @@ fn socks5_local_nslookup(
let addrs: Vec<SocketAddr> = unit let addrs: Vec<SocketAddr> = unit
.resolver() .resolver()
.resolve(&format!("{}:{}", hostname, port)) .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() { if addrs.is_empty() {
return Err(std::io::Error::new( return Err(std::io::Error::new(
ErrorKind::NotFound, io::ErrorKind::NotFound,
"DNS failure: no socket addrs found.", "DNS failure: no socket addrs found.",
)); ));
} }
@@ -458,7 +461,7 @@ fn socks5_local_nslookup(
Ok(addr) => Ok(addr), Ok(addr) => Ok(addr),
Err(err) => { Err(err) => {
return Err(std::io::Error::new( return Err(std::io::Error::new(
ErrorKind::NotFound, io::ErrorKind::NotFound,
format!("DNS failure: {}.", err), format!("DNS failure: {}.", err),
)) ))
} }
@@ -579,7 +582,7 @@ fn connect_socks5(
_port: u16, _port: u16,
) -> Result<TcpStream, std::io::Error> { ) -> Result<TcpStream, std::io::Error> {
Err(std::io::Error::new( Err(std::io::Error::new(
ErrorKind::Other, io::ErrorKind::Other,
"SOCKS5 feature disabled.", "SOCKS5 feature disabled.",
)) ))
} }
@@ -592,10 +595,12 @@ pub(crate) fn connect_test(unit: &Unit) -> Result<Stream, Error> {
#[cfg(not(test))] #[cfg(not(test))]
pub(crate) fn connect_test(unit: &Unit) -> Result<Stream, Error> { pub(crate) fn connect_test(unit: &Unit) -> Result<Stream, Error> {
Err(Error::UnknownScheme(unit.url.scheme().to_string())) Err(ErrorKind::UnknownScheme.msg(&format!("unknown scheme '{}'", unit.url.scheme())))
} }
#[cfg(not(feature = "tls"))] #[cfg(not(feature = "tls"))]
pub(crate) fn connect_https(unit: &Unit, _hostname: &str) -> Result<Stream, Error> { pub(crate) fn connect_https(unit: &Unit, _hostname: &str) -> Result<Stream, Error> {
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()))
} }

View File

@@ -1,5 +1,6 @@
#![allow(dead_code)] #![allow(dead_code)]
use crate::error::Error;
use crate::testserver::{read_request, TestServer}; use crate::testserver::{read_request, TestServer};
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::net::TcpStream; use std::net::TcpStream;

View File

@@ -1,10 +1,10 @@
use crate::error::Error;
use crate::stream::Stream;
use crate::unit::Unit; use crate::unit::Unit;
use crate::{error::Error};
use crate::{stream::Stream};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::io::{Cursor, Write}; use std::io::{Cursor, Write};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::{collections::HashMap};
mod agent_test; mod agent_test;
mod body_read; mod body_read;

View File

@@ -4,7 +4,7 @@ use std::{
}; };
use testserver::{self, TestServer}; use testserver::{self, TestServer};
use crate::test; use crate::{error::Error, test};
use super::super::*; use super::super::*;
@@ -34,7 +34,7 @@ fn redirect_many() {
.build() .build()
.get("test://host/redirect_many1") .get("test://host/redirect_many1")
.call(); .call();
assert!(matches!(result, Err(Error::TooManyRedirects))); assert!(matches!(result, Err(e) if e.kind() == ErrorKind::TooManyRedirects));
} }
#[test] #[test]
@@ -104,12 +104,11 @@ fn redirect_host() {
Ok(()) Ok(())
}); });
let url = format!("http://localhost:{}/", srv.port); let url = format!("http://localhost:{}/", srv.port);
let resp = crate::Agent::new().get(&url).call(); let result = crate::Agent::new().get(&url).call();
let err = resp.err();
assert!( assert!(
matches!(err, Some(Error::DnsFailed(_))), matches!(result, Err(ref e) if e.kind() == ErrorKind::DnsFailed),
"expected DnsFailed, got: {:?}", "expected Err(DnsFailed), got: {:?}",
err result
); );
} }

View File

@@ -157,13 +157,13 @@ fn non_ascii_header() {
test::set_handler("/non_ascii_header", |_unit| { test::set_handler("/non_ascii_header", |_unit| {
test::make_response(200, "OK", vec!["Wörse: Hädör"], vec![]) 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") .set("Bäd", "Headör")
.call(); .call();
assert!( assert!(
matches!(resp, Err(Error::BadHeader)), matches!(result, Err(ref e) if e.kind() == ErrorKind::BadHeader),
"expected Some(&BadHeader), got {:?}", "expected Err(BadHeader), got {:?}",
resp result
); );
} }

View File

@@ -1,8 +1,11 @@
use crate::testserver::*; use crate::testserver::*;
use std::io::{self, Write};
use std::net::TcpStream; use std::net::TcpStream;
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use std::{
error::Error,
io::{self, Write},
};
use super::super::*; use super::super::*;
@@ -96,10 +99,16 @@ fn read_timeout_during_headers() {
let server = TestServer::new(dribble_headers_respond); let server = TestServer::new(dribble_headers_respond);
let url = format!("http://localhost:{}/", server.port); let url = format!("http://localhost:{}/", server.port);
let agent = builder().timeout_read(Duration::from_millis(10)).build(); let agent = builder().timeout_read(Duration::from_millis(10)).build();
let resp = agent.get(&url).call(); let result = agent.get(&url).call();
match resp { match result {
Ok(_) => Err("successful response".to_string()), 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)), Err(e) => Err(format!("Unexpected error type: {:?}", e)),
} }
.expect("expected timeout but got something else"); .expect("expected timeout but got something else");
@@ -111,10 +120,16 @@ fn overall_timeout_during_headers() {
let server = TestServer::new(dribble_headers_respond); let server = TestServer::new(dribble_headers_respond);
let url = format!("http://localhost:{}/", server.port); let url = format!("http://localhost:{}/", server.port);
let agent = builder().timeout(Duration::from_millis(500)).build(); let agent = builder().timeout(Duration::from_millis(500)).build();
let resp = agent.get(&url).call(); let result = agent.get(&url).call();
match resp { match result {
Ok(_) => Err("successful response".to_string()), 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)), Err(e) => Err(format!("Unexpected error type: {:?}", e)),
} }
.expect("expected timeout but got something else"); .expect("expected timeout but got something else");

View File

@@ -7,6 +7,7 @@ use url::Url;
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]
use cookie::Cookie; use cookie::Cookie;
use crate::error::{Error, ErrorKind};
use crate::header; use crate::header;
use crate::resolve::ArcResolver; use crate::resolve::ArcResolver;
use crate::stream::{self, connect_test, Stream}; use crate::stream::{self, connect_test, Stream};
@@ -15,7 +16,7 @@ use crate::{
body::{self, BodySize, Payload, SizedReader}, body::{self, BodySize, Payload, SizedReader},
header::get_header, header::get_header,
}; };
use crate::{Error, Header, Response}; use crate::{Header, Response};
/// A Unit is fully-built Request, ready to execute. /// A Unit is fully-built Request, ready to execute.
/// ///
@@ -173,7 +174,7 @@ pub(crate) fn connect(
let host = unit let host = unit
.url .url
.host_str() .host_str()
.ok_or(Error::BadUrl("no host".to_string()))?; .ok_or(ErrorKind::BadUrl.msg("no host in URL"))?;
let url = &unit.url; let url = &unit.url;
let method = &unit.method; let method = &unit.method;
// open socket // open socket
@@ -234,16 +235,18 @@ pub(crate) fn connect(
// handle redirects // handle redirects
if resp.redirect() && unit.agent.config.redirects > 0 { if resp.redirect() && unit.agent.config.redirects > 0 {
if redirect_count == unit.agent.config.redirects { if redirect_count == unit.agent.config.redirects {
return Err(Error::TooManyRedirects); return Err(ErrorKind::TooManyRedirects.new());
} }
// the location header // the location header
let location = resp.header("location"); let location = resp.header("location");
if let Some(location) = location { if let Some(location) = location {
// join location header to current url in case it it relative // join location header to current url in case it it relative
let new_url = url let new_url = url.join(location).map_err(|e| {
.join(location) ErrorKind::BadUrl
.map_err(|_| Error::BadUrl(format!("Bad redirection: {}", location)))?; .msg(&format!("Bad redirection: {}", location))
.src(e)
})?;
// perform the redirect differently depending on 3xx code. // perform the redirect differently depending on 3xx code.
match resp.status() { match resp.status() {
@@ -302,7 +305,7 @@ fn extract_cookies(agent: &Agent, url: &Url) -> Option<Header> {
fn connect_socket(unit: &Unit, hostname: &str, use_pooled: bool) -> Result<(Stream, bool), Error> { fn connect_socket(unit: &Unit, hostname: &str, use_pooled: bool) -> Result<(Stream, bool), Error> {
match unit.url.scheme() { match unit.url.scheme() {
"http" | "https" | "test" => (), "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 { if use_pooled {
let agent = &unit.agent; 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), "http" => stream::connect_http(&unit, hostname),
"https" => stream::connect_https(&unit, hostname), "https" => stream::connect_https(&unit, hostname),
"test" => connect_test(&unit), "test" => connect_test(&unit),
_ => Err(Error::UnknownScheme(unit.url.scheme().to_string())), scheme => Err(ErrorKind::UnknownScheme.msg(&format!("unknown scheme {}", scheme))),
}; };
Ok((stream?, false)) Ok((stream?, false))
} }