diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fbc4fd9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,21 @@ +## License + +Copyright (c) 2019 Martin Algesten + +Licensed under either of + + * Apache License, Version 2.0 + ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + * MIT license + ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +## Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed under the Apache License, Version 2.0, and the MIT license, without +any additional terms or conditions. See LICENSE-APACHE and LICENSE-MIT for +details. + diff --git a/Cargo.toml b/Cargo.toml index a088b79..ca7fd68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,11 +2,11 @@ name = "ureq" version = "1.5.1" authors = ["Martin Algesten ", "Jacob Hoffman-Andrews "] -description = "Minimal HTTP request library" +description = "Simple, safe HTTP client" license = "MIT/Apache-2.0" repository = "https://github.com/algesten/ureq" readme = "README.md" -keywords = ["web", "request", "http", "rest", "client"] +keywords = ["web", "request", "rest", "https", "http", "client"] categories = ["web-programming::http-client"] edition = "2018" diff --git a/README.md b/README.md index 396acac..8c5d3a4 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,195 @@ +[comment]: # (README.md is autogenerated from src/lib.rs by `cargo readme > README.md`) + # ureq -![](https://github.com/algesten/ureq/workflows/CI/badge.svg) -[![CratesIO](https://img.shields.io/crates/v/ureq.svg)](https://crates.io/crates/ureq) -[![Documentation](https://docs.rs/ureq/badge.svg)](https://docs.rs/ureq) +A simple, safe HTTP client. -> Minimal request library in rust. +Ureq's first priority is being easy for you to use. It's great for +anyone who wants a low-overhead HTTP client that just gets the job done. Works +very well with HTTP APIs. Its features include cookies, JSON, HTTP proxies, +HTTPS, and charset decoding. -## Usage +Ureq is in pure Rust for safety and ease of understanding. It avoids using +`unsafe` directly. It [uses blocking I/O][blocking] instead of async I/O, because that keeps +the API simple and and keeps dependencies to a minimum. For TLS, ureq uses +[rustls]. + +[blocking]: #blocking-i-o-for-simplicity + +### Usage + +In its simplest form, ureq looks like this: ```rust -// sync post request of some json. -// requires feature: -// `ureq = { version = "*", features = ["json"] }` -let resp = ureq::post("https://myapi.example.com/ingest") - .set("X-My-Header", "Secret") - .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 { - println!("error {}: {}", resp.status(), resp.into_string()?); -} +let body: String = ureq::get("http://example.com") + .set("Accept", "text/html") + .call()? + .into_string()?; ``` -## About 1.0.0 +For more involved tasks, you'll want to create an [Agent]. An Agent +holds a connection pool for reuse, and a cookie store if you use the +"cookies" feature. An Agent can be cheaply cloned due to an internal +[Arc](std::sync::Arc) and all clones of an Agent share state among each other. Creating +an Agent also allows setting options like the TLS configuration. -This crate is now 1.x.x. It signifies there will be no more breaking -API changes (for better or worse). I personally use this code in -production system reading data from AWS. Whether the quality is good -enough for other use cases is a "YMMV". +```rust + use ureq::{Agent, AgentBuilder}; + use std::time::Duration; -## ureq's future + let agent: Agent = ureq::AgentBuilder::new() + .timeout_read(Duration::from_secs(5)) + .timeout_write(Duration::from_secs(5)) + .build(); + let body: String = agent.get("http://example.com/page") + .call()? + .into_string()?; -I asked for feedback on [ureq's future -direction](https://www.reddit.com/r/rust/comments/eu6qg8/future_of_ureq_http_client_library/) -and came to the conclusion that there's enough interest in a simple -blocking http client to keep it going. Another motivation is that I -use it extensively for my own work, to talk to S3. + // Reuses the connection from previous request. + let response: String = agent.put("http://example.com/upload") + .set("Authorization", "example-token") + .call()? + .into_string()?; +``` -I'll keep maintaining ureq. I will try to keep dependencies somewhat -fresh and try to address bad bugs. I will however not personally -implement new features in ureq, but I do welcome PR with open arms. +Ureq supports sending and receiving json, if you enable the "json" feature: -The code base is extremely simple, one might even call naive. It's a -good project to hack on as first learning experience in Rust. I will -uphold some base line of code hygiene, but won't block a PR due to -something being a bit inelegant. +```rust + // Requires the `json` feature enabled. + let resp: String = ureq::post("http://myapi.example.com/ingest") + .set("X-My-Header", "Secret") + .send_json(ureq::json!({ + "name": "martin", + "rust": true + }))? + .into_string()?; +``` -## Features +### Features To enable a minimal dependency tree, some features are off by default. -You can control them when including `ureq` as a dependency. +You can control them when including ureq as a dependency. -``` - ureq = { version = "*", features = ["json", "charset"] } -``` +`ureq = { version = "*", features = ["json", "charset"] }` * `tls` enables https. This is enabled by default. -* `cookies` enables handling cookies between requests in an agent. -* `json` enables `response.into_json()` and `request.send_json()` via serde_json. -* `charset` enables interpreting the charset part of - `Content-Type: text/plain; charset=iso-8859-1`. Without this, the library - defaults to rust's built in `utf-8`. +* `cookies` enables cookies. +* `json` enables [Response::into_json()] and [Request::send_json()] via serde_json. +* `charset` enables interpreting the charset part of the Content-Type header + (e.g. `Content-Type: text/plain; charset=iso-8859-1`). Without this, the + library defaults to Rust's built in `utf-8`. -## Motivation +## Plain requests - * Minimal dependency tree - * Obvious API - * Blocking API - * Convenience over correctness - * No use of unsafe +Most standard methods (GET, POST, PUT etc), are supported as functions from the +top of the library ([get()], [post()], [put()], etc). -This library tries to provide a convenient request library with a minimal dependency -tree and an obvious API. It is inspired by libraries like +These top level http method functions create a [Request] instance +which follows a build pattern. The builders are finished using: + +* [`.call()`][Request::call()] without a request body. +* [`.send()`][Request::send()] with a request body as [Read][std::io::Read] (chunked encoding support for non-known sized readers). +* [`.send_string()`][Request::send_string()] body as string. +* [`.send_bytes()`][Request::send_bytes()] body as bytes. +* [`.send_form()`][Request::send_form()] key-value pairs as application/x-www-form-urlencoded. + +## JSON + +By enabling the `ureq = { version = "*", features = ["json"] }` feature, +the library supports serde json. + +* [`request.send_json()`][Request::send_json()] send body as serde json. +* [`response.into_json()`][Response::into_json()] transform response to json. + +## Content-Length and Transfer-Encoding + +The library will send a Content-Length header on requests with bodies of +known size, in other words, those sent with +[`.send_string()`][Request::send_string()], +[`.send_bytes()`][Request::send_bytes()], +[`.send_form()`][Request::send_form()], or +[`.send_json()`][Request::send_json()]. If you send a +request body with [`.send()`][Request::send()], +which takes a [Read][std::io::Read] of unknown size, ureq will send Transfer-Encoding: +chunked, and encode the body accordingly. Bodyless requests +(GETs and HEADs) are sent with [`.call()`][Request::call()] +and ureq adds neither a Content-Length nor a Transfer-Encoding header. + +If you set your own Content-Length or Transfer-Encoding header before +sending the body, ureq will respect that header by not overriding it, +and by encoding the body or not, as indicated by the headers you set. + +```rust +let resp = ureq::post("http://my-server.com/ingest") + .set("Transfer-Encoding", "chunked") + .send_string("Hello world"); +``` + +## Character encoding + +By enabling the `ureq = { version = "*", features = ["charset"] }` feature, +the library supports sending/receiving other character sets than `utf-8`. + +For [`response.into_string()`][Response::into_string()] we read the +header `Content-Type: text/plain; charset=iso-8859-1` and if it contains a charset +specification, we try to decode the body using that encoding. In the absence of, or failing +to interpret the charset, we fall back on `utf-8`. + +Similarly when using [`request.send_string()`][Request::send_string()], +we first check if the user has set a `; charset=` and attempt +to encode the request body using that. + +## Blocking I/O for simplicity + +Rust supports [asynchronous (async) I/O][async], but ureq does not use it. Async I/O +allows serving many concurrent requests without high costs in memory and OS threads. But +it comes at a cost in complexity. Async programs need to pull in a runtime (usually +[async-std] or [tokio]). They also need async variants of any method that might block, and of +[any method that might call another method that might block][what-color]. That means async +programs usually have a lot of dependencies - which adds to compile times, and increases +risk. + +The costs of async are worth paying, if you're writing an HTTP server that must serve +many many clients with minimal overhead. However, for HTTP _clients_, we believe that the +cost is usually not worth paying. The low-cost alternative to async I/O is blocking I/O, +which has a different price: it requires an OS thread per concurrent request. However, +that price is usually not high: most HTTP clients make requests sequentially, or with +low concurrency. + +That's why ureq uses blocking I/O and plans to stay that way. Other HTTP clients offer both +an async API and a blocking API, but we want to offer a blocking API without pulling in all +the dependencies required by an async API. + +[async]: https://rust-lang.github.io/async-book/01_getting_started/02_why_async.html +[async-std]: https://github.com/async-rs/async-std#async-std +[tokio]: https://github.com/tokio-rs/tokio#tokio +[what-color]: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ + +------------------------------------------------------------------------------ + +Ureq is inspired by other great HTTP clients like [superagent](http://visionmedia.github.io/superagent/) and -[fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). +[the fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). -### Sync forever +If ureq is not what you're looking for, check out these other Rust HTTP clients: +[surf](https://crates.io/crates/surf), [reqwest](https://crates.io/crates/reqwest), +[isahc](https://crates.io/crates/isahc), [attohttpc](https://crates.io/crates/attohttpc), +[actix-web](https://crates.io/crates/actix-web), and [hyper](https://crates.io/crates/hyper). -This library uses blocking socket reads and writes. When it was -created, there wasn't any async/await support in rust, and for my own -purposes, blocking IO was fine. At this point, one good reason to keep -this library going is that it is blocking (the other is that it does not -use unsafe). -## TODO - -- [ ] Forms with application/x-www-form-urlencoded -- [ ] multipart/form-data -- [ ] Expect 100-continue -- [x] Use `rustls` when [ring with versioned asm symbols](https://github.com/briansmith/ring/pull/619) is released. (PR is not resolved, but most implementations have settled on 0.13) - -## License - -Copyright (c) 2019 Martin Algesten - -Licensed under either of - - * Apache License, Version 2.0 - ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - * MIT license - ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - -at your option. - -## Contribution - -Unless you explicitly state otherwise, any contribution intentionally submitted -for inclusion in the work by you, as defined in the Apache-2.0 license, shall be -dual licensed as above, without any additional terms or conditions. +[rustls]: https://docs.rs/rustls/ +[std::sync::Arc]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html +[std::io::Read]: https://doc.rust-lang.org/stable/std/io/trait.Read.html +[Agent]: https://docs.rs/ureq/latest/ureq/struct.Agent.html +[get()]: https://docs.rs/ureq/latest/ureq/fn.get.html +[post()]: https://docs.rs/ureq/latest/ureq/fn.post.html +[put()]: https://docs.rs/ureq/latest/ureq/fn.put.html +[Request]: https://docs.rs/ureq/latest/ureq/struct.Request.html +[Request::call()]: https://docs.rs/ureq/latest/ureq/struct.Request.html#method.call +[Request::send()]: https://docs.rs/ureq/latest/ureq/struct.Request.html#method.send +[Request::send_bytes()]: https://docs.rs/ureq/latest/ureq/struct.Request.html#method.send_bytes +[Request::send_string()]: https://docs.rs/ureq/latest/ureq/struct.Request.html#method.send_string +[Request::send_json()]: https://docs.rs/ureq/latest/ureq/struct.Request.html#method.send_json +[Request::send_form()]: https://docs.rs/ureq/latest/ureq/struct.Request.html#method.send_form +[Response::into_json()]: https://docs.rs/ureq/latest/ureq/struct.Response.html#method.into_json +[Response::into_string()]: https://docs.rs/ureq/latest/ureq/struct.Response.html#method.into_string diff --git a/README.tpl b/README.tpl new file mode 100644 index 0000000..92a944d --- /dev/null +++ b/README.tpl @@ -0,0 +1,22 @@ +[comment]: # (README.md is autogenerated from src/lib.rs by `cargo readme > README.md`) + +# {{crate}} + +{{readme}} + +[rustls]: https://docs.rs/rustls/ +[std::sync::Arc]: https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html +[std::io::Read]: https://doc.rust-lang.org/stable/std/io/trait.Read.html +[Agent]: https://docs.rs/ureq/latest/ureq/struct.Agent.html +[get()]: https://docs.rs/ureq/latest/ureq/fn.get.html +[post()]: https://docs.rs/ureq/latest/ureq/fn.post.html +[put()]: https://docs.rs/ureq/latest/ureq/fn.put.html +[Request]: https://docs.rs/ureq/latest/ureq/struct.Request.html +[Request::call()]: https://docs.rs/ureq/latest/ureq/struct.Request.html#method.call +[Request::send()]: https://docs.rs/ureq/latest/ureq/struct.Request.html#method.send +[Request::send_bytes()]: https://docs.rs/ureq/latest/ureq/struct.Request.html#method.send_bytes +[Request::send_string()]: https://docs.rs/ureq/latest/ureq/struct.Request.html#method.send_string +[Request::send_json()]: https://docs.rs/ureq/latest/ureq/struct.Request.html#method.send_json +[Request::send_form()]: https://docs.rs/ureq/latest/ureq/struct.Request.html#method.send_form +[Response::into_json()]: https://docs.rs/ureq/latest/ureq/struct.Response.html#method.into_json +[Response::into_string()]: https://docs.rs/ureq/latest/ureq/struct.Response.html#method.into_string 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 3d3ae1e..33dc1f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,84 +1,136 @@ #![forbid(unsafe_code)] #![warn(clippy::all)] -//! ureq is a minimal request library. +//! A simple, safe HTTP client. //! -//! The goals of this library are: +//! Ureq's first priority is being easy for you to use. It's great for +//! anyone who wants a low-overhead HTTP client that just gets the job done. Works +//! very well with HTTP APIs. Its features include cookies, JSON, HTTP proxies, +//! HTTPS, and charset decoding. //! -//! * Minimal dependency tree -//! * Obvious API -//! * Blocking API -//! * No use of unsafe +//! Ureq is in pure Rust for safety and ease of understanding. It avoids using +//! `unsafe` directly. It [uses blocking I/O][blocking] instead of async I/O, because that keeps +//! the API simple and and keeps dependencies to a minimum. For TLS, ureq uses +//! [rustls]. //! -//! ``` -//! // requires feature: `ureq = { version = "*", features = ["json"] }` -//! # #[cfg(feature = "json")] { -//! use ureq::json; +//! [blocking]: #blocking-i-o-for-simplicity //! -//! fn main() -> std::io::Result<()> { -//! // sync post request of some json. -//! let resp = ureq::post("https://myapi.example.com/ingest") -//! .set("X-My-Header", "Secret") -//! .send_json(json!({ -//! "name": "martin", -//! "rust": true -//! })); +//! ## Usage //! -//! if let Ok(resp) = resp { -//! println!("success: {}", resp.into_string()?); -//! } else { -//! // This can include errors like failure to parse URL or connect timeout. -//! println!("error {}", resp.err().unwrap()); -//! } -//! Ok(()) -//! } +//! In its simplest form, ureq looks like this: +//! +//! ```rust +//! # fn main() -> Result<(), ureq::Error> { +//! # ureq::is_test(true); +//! let body: String = ureq::get("http://example.com") +//! .set("Accept", "text/html") +//! .call()? +//! .into_string()?; +//! # Ok(()) //! # } //! ``` //! +//! For more involved tasks, you'll want to create an [Agent]. An Agent +//! holds a connection pool for reuse, and a cookie store if you use the +//! "cookies" feature. An Agent can be cheaply cloned due to an internal +//! [Arc](std::sync::Arc) and all clones of an Agent share state among each other. Creating +//! an Agent also allows setting options like the TLS configuration. +//! +//! ```no_run +//! # fn main() -> std::result::Result<(), ureq::Error> { +//! # ureq::is_test(true); +//! use ureq::{Agent, AgentBuilder}; +//! use std::time::Duration; +//! +//! let agent: Agent = ureq::AgentBuilder::new() +//! .timeout_read(Duration::from_secs(5)) +//! .timeout_write(Duration::from_secs(5)) +//! .build(); +//! let body: String = agent.get("http://example.com/page") +//! .call()? +//! .into_string()?; +//! +//! // Reuses the connection from previous request. +//! let response: String = agent.put("http://example.com/upload") +//! .set("Authorization", "example-token") +//! .call()? +//! .into_string()?; +//! # Ok(()) +//! # } +//! ``` +//! +//! Ureq supports sending and receiving json, if you enable the "json" feature: +//! +//! ```rust +//! # #[cfg(feature = "json")] +//! # fn main() -> std::result::Result<(), ureq::Error> { +//! # ureq::is_test(true); +//! // Requires the `json` feature enabled. +//! let resp: String = ureq::post("http://myapi.example.com/ingest") +//! .set("X-My-Header", "Secret") +//! .send_json(ureq::json!({ +//! "name": "martin", +//! "rust": true +//! }))? +//! .into_string()?; +//! # Ok(()) +//! # } +//! # #[cfg(not(feature = "json"))] +//! # fn main() {} +//! ``` +//! +//! ## Features +//! +//! To enable a minimal dependency tree, some features are off by default. +//! You can control them when including ureq as a dependency. +//! +//! `ureq = { version = "*", features = ["json", "charset"] }` +//! +//! * `tls` enables https. This is enabled by default. +//! * `cookies` enables cookies. +//! * `json` enables [Response::into_json()] and [Request::send_json()] via serde_json. +//! * `charset` enables interpreting the charset part of the Content-Type header +//! (e.g. `Content-Type: text/plain; charset=iso-8859-1`). Without this, the +//! library defaults to Rust's built in `utf-8`. +//! //! # Plain requests //! //! Most standard methods (GET, POST, PUT etc), are supported as functions from the -//! top of the library ([`ureq::get`](fn.get.html), [`ureq::post`](fn.post.html), -//! [`ureq::put`](fn.put.html), etc). +//! top of the library ([get()], [post()], [put()], etc). //! -//! These top level http method functions create a [Request](struct.Request.html) instance +//! These top level http method functions create a [Request] instance //! which follows a build pattern. The builders are finished using: //! -//! * [`.call()`](struct.Request.html#method.call) without a request body. -//! * [`.send()`](struct.Request.html#method.send) with a request body as `Read` (chunked encoding support for non-known sized readers). -//! * [`.send_string()`](struct.Request.html#method.send_string) body as string. -//! * [`.send_bytes()`](struct.Request.html#method.send_bytes) body as bytes. -//! * [`.send_form()`](struct.Request.html#method.send_form) key-value pairs as application/x-www-form-urlencoded. +//! * [`.call()`][Request::call()] without a request body. +//! * [`.send()`][Request::send()] with a request body as [Read][std::io::Read] (chunked encoding support for non-known sized readers). +//! * [`.send_string()`][Request::send_string()] body as string. +//! * [`.send_bytes()`][Request::send_bytes()] body as bytes. +//! * [`.send_form()`][Request::send_form()] key-value pairs as application/x-www-form-urlencoded. //! //! # JSON //! //! By enabling the `ureq = { version = "*", features = ["json"] }` feature, //! the library supports serde json. //! -//! * [`request.send_json()`](struct.Request.html#method.send_json) send body as serde json. -//! * [`response.into_json()`](struct.Response.html#method.into_json) transform response to json. +//! * [`request.send_json()`][Request::send_json()] send body as serde json. +//! * [`response.into_json()`][Response::into_json()] transform response to json. //! -//! # Agents +//! # Content-Length and Transfer-Encoding //! -//! To maintain a state, cookies, between requests, you use an [agent](struct.Agent.html). -//! Agents also follow the build pattern. Agents are created with -//! [`ureq::agent()`](struct.Agent.html). +//! The library will send a Content-Length header on requests with bodies of +//! known size, in other words, those sent with +//! [`.send_string()`][Request::send_string()], +//! [`.send_bytes()`][Request::send_bytes()], +//! [`.send_form()`][Request::send_form()], or +//! [`.send_json()`][Request::send_json()]. If you send a +//! request body with [`.send()`][Request::send()], +//! which takes a [Read][std::io::Read] of unknown size, ureq will send Transfer-Encoding: +//! chunked, and encode the body accordingly. Bodyless requests +//! (GETs and HEADs) are sent with [`.call()`][Request::call()] +//! and ureq adds neither a Content-Length nor a Transfer-Encoding header. //! -//! # Content-Length -//! -//! The library will set the content length on the request when using -//! [`.send_string()`](struct.Request.html#method.send_string) or -//! [`.send_json()`](struct.Request.html#method.send_json). In other cases the user -//! can optionally `request.set("Content-Length", 1234)`. -//! -//! For responses, if the `Content-Length` header is present, the methods that reads the -//! body (as string, json or read trait) are all limited to the length specified in the header. -//! -//! # Transfer-Encoding: chunked -//! -//! Dechunking is a response body is done automatically if the response headers contains -//! a `Transfer-Encoding` header. -//! -//! Sending a chunked request body is done by setting the header prior to sending a body. +//! If you set your own Content-Length or Transfer-Encoding header before +//! sending the body, ureq will respect that header by not overriding it, +//! and by encoding the body or not, as indicated by the headers you set. //! //! ``` //! let resp = ureq::post("http://my-server.com/ingest") @@ -91,15 +143,52 @@ //! By enabling the `ureq = { version = "*", features = ["charset"] }` feature, //! the library supports sending/receiving other character sets than `utf-8`. //! -//! For [`response.into_string()`](struct.Response.html#method.into_string) we read the +//! For [`response.into_string()`][Response::into_string()] we read the //! header `Content-Type: text/plain; charset=iso-8859-1` and if it contains a charset //! specification, we try to decode the body using that encoding. In the absence of, or failing //! to interpret the charset, we fall back on `utf-8`. //! -//! Similarly when using [`request.send_string()`](struct.Request.html#method.send_string), +//! Similarly when using [`request.send_string()`][Request::send_string()], //! we first check if the user has set a `; charset=` and attempt //! to encode the request body using that. //! +//! # Blocking I/O for simplicity +//! +//! Rust supports [asynchronous (async) I/O][async], but ureq does not use it. Async I/O +//! allows serving many concurrent requests without high costs in memory and OS threads. But +//! it comes at a cost in complexity. Async programs need to pull in a runtime (usually +//! [async-std] or [tokio]). They also need async variants of any method that might block, and of +//! [any method that might call another method that might block][what-color]. That means async +//! programs usually have a lot of dependencies - which adds to compile times, and increases +//! risk. +//! +//! The costs of async are worth paying, if you're writing an HTTP server that must serve +//! many many clients with minimal overhead. However, for HTTP _clients_, we believe that the +//! cost is usually not worth paying. The low-cost alternative to async I/O is blocking I/O, +//! which has a different price: it requires an OS thread per concurrent request. However, +//! that price is usually not high: most HTTP clients make requests sequentially, or with +//! low concurrency. +//! +//! That's why ureq uses blocking I/O and plans to stay that way. Other HTTP clients offer both +//! an async API and a blocking API, but we want to offer a blocking API without pulling in all +//! the dependencies required by an async API. +//! +//! [async]: https://rust-lang.github.io/async-book/01_getting_started/02_why_async.html +//! [async-std]: https://github.com/async-rs/async-std#async-std +//! [tokio]: https://github.com/tokio-rs/tokio#tokio +//! [what-color]: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ +//! +//! ------------------------------------------------------------------------------ +//! +//! Ureq is inspired by other great HTTP clients like +//! [superagent](http://visionmedia.github.io/superagent/) and +//! [the fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). +//! +//! If ureq is not what you're looking for, check out these other Rust HTTP clients: +//! [surf](https://crates.io/crates/surf), [reqwest](https://crates.io/crates/reqwest), +//! [isahc](https://crates.io/crates/isahc), [attohttpc](https://crates.io/crates/attohttpc), +//! [actix-web](https://crates.io/crates/actix-web), and [hyper](https://crates.io/crates/hyper). +//! mod agent; mod body; @@ -126,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; @@ -247,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 67e2db5..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; @@ -58,7 +58,10 @@ impl Request { } } - /// Executes the request and blocks the caller until done. + /// Sends the request with no body and blocks the caller until done. + /// + /// Use this with GET, HEAD, or TRACE. It sends neither Content-Length + /// nor Transfer-Encoding. /// /// ``` /// # fn main() -> Result<(), ureq::Error> { @@ -76,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 f5997e3..18320e1 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}; @@ -400,13 +400,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), ) }) @@ -466,19 +466,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 { @@ -533,7 +535,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", )); } @@ -542,8 +544,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'; @@ -587,7 +590,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) => { @@ -736,7 +739,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 369fe62..5a45e78 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 ab9d034..3b46e59 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)) }