Skip to content

Commit

Permalink
Switch to Result-based API. (fortanix#132)
Browse files Browse the repository at this point in the history
Gets rid of synthetic_error, and makes the various send_* methods return `Result<Response, Error>`.
Introduces a new error type "HTTP", which represents an error due to status codes 4xx or 5xx.
The HTTP error type contains a boxed Response, so users can read the actual response if they want.
Adds an `error_for_status` setting to disable the functionality of treating 4xx and 5xx as errors.
Adds .unwrap() to a lot of tests.

Fixes fortanix#128.
  • Loading branch information
jsha authored Oct 17, 2020
1 parent 257d4e5 commit e36c1c2
Show file tree
Hide file tree
Showing 19 changed files with 222 additions and 344 deletions.
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,12 @@ let resp = ureq::post("https://myapi.example.com/ingest")
.send_json(serde_json::json!({
"name": "martin",
"rust": true
}));
}))?;

// .ok() tells if response is 200-299.
if resp.ok() {
println!("success: {}", resp.into_string()?);
} else {
// This can include errors like failure to parse URL or
// connect timeout. They are treated as synthetic
// HTTP-level error statuses.
println!("error {}: {}", resp.status(), resp.into_string()?);
}
```
Expand Down
9 changes: 3 additions & 6 deletions examples/smoke-test/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ impl From<io::Error> for Oops {
}
}

impl From<&ureq::Error> for Oops {
fn from(e: &ureq::Error) -> Oops {
impl From<ureq::Error> for Oops {
fn from(e: ureq::Error) -> Oops {
Oops(e.to_string())
}
}
Expand All @@ -44,10 +44,7 @@ fn get(agent: &ureq::Agent, url: &String) -> Result<Vec<u8>> {
.get(url)
.timeout_connect(std::time::Duration::from_secs(5))
.timeout(Duration::from_secs(20))
.call();
if let Some(err) = response.synthetic_error() {
return Err(err.into());
}
.call()?;
let mut reader = response.into_reader();
let mut bytes = vec![];
reader.read_to_end(&mut bytes)?;
Expand Down
18 changes: 8 additions & 10 deletions src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@ use crate::resolve::ArcResolver;
/// .auth("martin", "rubbermashgum")
/// .call(); // blocks. puts auth cookies in agent.
///
/// if !auth.ok() {
/// if auth.is_err() {
/// println!("Noes!");
/// }
///
/// let secret = agent
/// .get("/my-protected-page")
/// .call(); // blocks and waits for request.
///
/// if !secret.ok() {
/// if secret.is_err() {
/// println!("Wot?!");
/// } else {
/// println!("Secret is: {}", secret.unwrap().into_string().unwrap());
/// }
///
/// println!("Secret is: {}", secret.into_string().unwrap());
/// ```
#[derive(Debug, Default, Clone)]
pub struct Agent {
Expand Down Expand Up @@ -110,8 +110,8 @@ impl Agent {
/// .get("/my-page")
/// .call();
///
/// if r.ok() {
/// println!("yay got {}", r.into_string().unwrap());
/// if let Ok(resp) = r {
/// println!("yay got {}", resp.into_string().unwrap());
/// } else {
/// println!("Oh no error!");
/// }
Expand Down Expand Up @@ -376,8 +376,7 @@ mod tests {
let agent = crate::agent();
let url = "https://ureq.s3.eu-central-1.amazonaws.com/sherlock.txt";
// req 1
let resp = agent.get(url).call();
assert!(resp.ok());
let resp = agent.get(url).call().unwrap();
let mut reader = resp.into_reader();
let mut buf = vec![];
// reading the entire content will return the connection to the pool
Expand All @@ -390,8 +389,7 @@ mod tests {
assert_eq!(poolsize(&agent), 1);

// req 2 should be done with a reused connection
let resp = agent.get(url).call();
assert!(resp.ok());
let resp = agent.get(url).call().unwrap();
assert_eq!(poolsize(&agent), 0);
let mut reader = resp.into_reader();
let mut buf = vec![];
Expand Down
100 changes: 30 additions & 70 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
use crate::response::Response;
use std::fmt;
use std::io::{self, ErrorKind};

/// Errors that are translated to ["synthetic" responses](struct.Response.html#method.synthetic).
#[derive(Debug)]
pub enum Error {
/// The url could not be understood. Synthetic error `400`.
/// The url could not be understood.
BadUrl(String),
/// The url scheme could not be understood. Synthetic error `400`.
/// The url scheme could not be understood.
UnknownScheme(String),
/// DNS lookup failed. Synthetic error `400`.
/// DNS lookup failed.
DnsFailed(String),
/// Connection to server failed. Synthetic error `500`.
/// Connection to server failed.
ConnectionFailed(String),
/// Too many redirects. Synthetic error `500`.
/// Too many redirects.
TooManyRedirects,
/// A status line we don't understand `HTTP/1.1 200 OK`. Synthetic error `500`.
/// A status line we don't understand `HTTP/1.1 200 OK`.
BadStatus,
/// A header line that couldn't be parsed. Synthetic error `500`.
/// A header line that couldn't be parsed.
BadHeader,
/// Some unspecified `std::io::Error`. Synthetic error `500`.
/// Some unspecified `std::io::Error`.
Io(io::Error),
/// Proxy information was not properly formatted
BadProxy,
Expand All @@ -28,6 +28,10 @@ pub enum Error {
ProxyConnect,
/// Incorrect credentials for proxy
InvalidProxyCreds,
/// 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<Response>),
/// TLS Error
#[cfg(feature = "native-tls")]
TlsError(native_tls::Error),
Expand All @@ -42,66 +46,6 @@ impl Error {
_ => false,
}
}

/// For synthetic responses, this is the error code.
pub fn status(&self) -> u16 {
match self {
Error::BadUrl(_) => 400,
Error::UnknownScheme(_) => 400,
Error::DnsFailed(_) => 400,
Error::ConnectionFailed(_) => 500,
Error::TooManyRedirects => 500,
Error::BadStatus => 500,
Error::BadHeader => 500,
Error::Io(_) => 500,
Error::BadProxy => 500,
Error::BadProxyCreds => 500,
Error::ProxyConnect => 500,
Error::InvalidProxyCreds => 500,
#[cfg(feature = "native-tls")]
Error::TlsError(_) => 500,
}
}

/// For synthetic responses, this is the status text.
pub fn status_text(&self) -> &str {
match self {
Error::BadUrl(_) => "Bad URL",
Error::UnknownScheme(_) => "Unknown Scheme",
Error::DnsFailed(_) => "Dns Failed",
Error::ConnectionFailed(_) => "Connection Failed",
Error::TooManyRedirects => "Too Many Redirects",
Error::BadStatus => "Bad Status",
Error::BadHeader => "Bad Header",
Error::Io(_) => "Network Error",
Error::BadProxy => "Malformed proxy",
Error::BadProxyCreds => "Failed to parse proxy credentials",
Error::ProxyConnect => "Proxy failed to connect",
Error::InvalidProxyCreds => "Provided proxy credentials are incorrect",
#[cfg(feature = "native-tls")]
Error::TlsError(_) => "TLS Error",
}
}

/// For synthetic responses, this is the body text.
pub fn body_text(&self) -> String {
match self {
Error::BadUrl(url) => format!("Bad URL: {}", url),
Error::UnknownScheme(scheme) => format!("Unknown Scheme: {}", scheme),
Error::DnsFailed(err) => format!("Dns Failed: {}", err),
Error::ConnectionFailed(err) => format!("Connection Failed: {}", err),
Error::TooManyRedirects => "Too Many Redirects".to_string(),
Error::BadStatus => "Bad Status".to_string(),
Error::BadHeader => "Bad Header".to_string(),
Error::Io(ioe) => format!("Network Error: {}", ioe),
Error::BadProxy => "Malformed proxy".to_string(),
Error::BadProxyCreds => "Failed to parse proxy credentials".to_string(),
Error::ProxyConnect => "Proxy failed to connect".to_string(),
Error::InvalidProxyCreds => "Provided proxy credentials are incorrect".to_string(),
#[cfg(feature = "native-tls")]
Error::TlsError(err) => format!("TLS Error: {}", err),
}
}
}

impl From<io::Error> for Error {
Expand All @@ -112,7 +56,23 @@ impl From<io::Error> for Error {

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.body_text())
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()),
#[cfg(feature = "native-tls")]
Error::TlsError(err) => write!(f, "TLS Error: {}", err),
}
}
}

Expand Down
31 changes: 7 additions & 24 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,11 @@
//! "rust": true
//! }));
//!
//! // .ok() tells if response is 200-299.
//! if resp.ok() {
//! if let Ok(resp) = resp {
//! println!("success: {}", resp.into_string()?);
//! } else {
//! // This can include errors like failure to parse URL or connect timeout.
//! // They are treated as synthetic HTTP-level error statuses.
//! println!("error {}: {}", resp.status(), resp.into_string()?);
//! println!("error {}", resp.err().unwrap());
//! }
//! Ok(())
//! }
Expand Down Expand Up @@ -103,20 +101,6 @@
//! we first check if the user has set a `; charset=<whatwg charset>` and attempt
//! to encode the request body using that.
//!
//! # Synthetic errors
//!
//! Rather than exposing a custom error type through results, this library has opted for
//! representing potential connection/TLS/etc errors as HTTP response codes. These invented codes
//! are called "[synthetic](struct.Response.html#method.synthetic)."
//!
//! The idea is that from a library user's point of view the distinction of whether a failure
//! originated in the remote server (500, 502) etc, or some transient network failure, the code
//! path of handling that would most often be the same.
//!
//! As a result, reading from a Response may yield an error message generated by the ureq library.
//! To handle these errors, use the
//! [`response.synthetic_error()`](struct.Response.html#method.synthetic_error) method.
//!

mod agent;
mod body;
Expand Down Expand Up @@ -158,7 +142,7 @@ pub fn agent() -> Agent {
/// Make a request setting the HTTP method via a string.
///
/// ```
/// ureq::request("GET", "https://www.google.com").call();
/// ureq::request("GET", "http://example.com").call().unwrap();
/// ```
pub fn request(method: &str, path: &str) -> Request {
Agent::new().request(method, path)
Expand Down Expand Up @@ -210,7 +194,7 @@ mod tests {

#[test]
fn connect_http_google() {
let resp = get("http://www.google.com/").call();
let resp = get("http://www.google.com/").call().unwrap();
assert_eq!(
"text/html; charset=ISO-8859-1",
resp.header("content-type").unwrap()
Expand All @@ -221,7 +205,7 @@ mod tests {
#[test]
#[cfg(any(feature = "tls", feature = "native-tls"))]
fn connect_https_google() {
let resp = get("https://www.google.com/").call();
let resp = get("https://www.google.com/").call().unwrap();
assert_eq!(
"text/html; charset=ISO-8859-1",
resp.header("content-type").unwrap()
Expand All @@ -232,8 +216,7 @@ mod tests {
#[test]
#[cfg(any(feature = "tls", feature = "native-tls"))]
fn connect_https_invalid_name() {
let resp = get("https://example.com{REQUEST_URI}/").call();
assert_eq!(400, resp.status());
assert!(resp.synthetic());
let result = get("https://example.com{REQUEST_URI}/").call();
assert!(matches!(result.unwrap_err(), Error::DnsFailed(_)));
}
}
Loading

0 comments on commit e36c1c2

Please sign in to comment.