API changes for 2.0

* Remove Request::build
* All mutations on Request follow builder pattern

The previous `build()` on request was necessary because mutating
functions did not follow a proper builder pattern (taking `&mut self`
instead of `mut self`). With a proper builder pattern, the need for
`.build()` goes away.

* All Request body and call methods consume self

Anything which "executes" the request will now consume the `Request`
to produce a `Result<Response>`.

* Move all config from request to agent builder

Timeouts, redirect config, proxy settings and TLS config are now on
`AgentBuilder`.

* Rename max_pool_connections -> max_idle_connections
* Rename max_pool_connections_per_host ->  max_idle_connections_per_host

Consistent internal and external naming.

* Introduce new AgentConfig for static config created by builder.

`Agent` can be seen as having two parts. Static config and a mutable
shared state between all states. The static config goes into
`AgentConfig` and the mutable shared state into `AgentState`.

* Replace all use of `Default` for `new`.

Deriving or implementing `Default` makes for a secondary instantiation
API.  It is useful in some cases, but gets very confusing when there
is both `new` _and_ a `Default`. It's especially devious for derived
values where a reasonable default is not `0`, `false` or `None`.

* Remove feature native_tls, we want only native rustls.

This feature made for very clunky handling throughout the code. From a
security point of view, it's better to stick with one single TLS API.
Rustls recently got an official audit (very positive).

https://github.com/ctz/rustls/tree/master/audit

Rustls deliberately omits support for older, insecure TLS such as TLS
1.1 or RC4. This might be a problem for a user of ureq, but on balance
not considered important enough to keep native_tls.

* Remove auth and support for basic auth.

The API just wasn't enough. A future reintroduction should at least
also provide a `Bearer` mechanism and possibly more.

* Rename jar -> cookie_store
* Rename jar -> cookie_tin

Just make some field names sync up with the type.

* Drop "cookies" as default feature

The need for handling cookies is probably rare, let's not enable it by
default.

* Change all feature checks for "cookie" to "cookies"

The outward facing feature is "cookies" and I think it's better form
that the code uses the official feature name instead of the optional
library "cookies".

* Keep `set` on Agent level as well as AgentBuilder.

The idea is that an auth exchange might result in a header that need
to be set _after_ the agent has been built.
This commit is contained in:
Martin Algesten
2020-10-25 11:08:50 +01:00
parent 703ca41960
commit 1369c32351
24 changed files with 398 additions and 647 deletions

View File

@@ -10,9 +10,7 @@ use super::super::*;
#[test]
fn agent_reuse_headers() {
let agent = AgentBuilder::new()
.set("Authorization", "Foo 12345")
.build();
let agent = builder().set("Authorization", "Foo 12345").build();
test::set_handler("/agent_reuse_headers", |unit| {
assert!(unit.has("Authorization"));
@@ -46,7 +44,7 @@ fn idle_timeout_handler(mut stream: TcpStream) -> io::Result<()> {
fn connection_reuse() {
let testserver = TestServer::new(idle_timeout_handler);
let url = format!("http://localhost:{}", testserver.port);
let agent = Agent::default();
let agent = Agent::new();
let resp = agent.get(&url).call().unwrap();
// use up the connection so it gets returned to the pool
@@ -96,7 +94,7 @@ fn custom_resolver() {
assert_eq!(&server.join().unwrap(), b"GET / HTTP/1.1\r\n");
}
#[cfg(feature = "cookie")]
#[cfg(feature = "cookies")]
#[cfg(test)]
fn cookie_and_redirect(mut stream: TcpStream) -> io::Result<()> {
let headers = read_headers(&stream);
@@ -139,14 +137,14 @@ fn cookie_and_redirect(mut stream: TcpStream) -> io::Result<()> {
Ok(())
}
#[cfg(feature = "cookie")]
#[cfg(feature = "cookies")]
#[test]
fn test_cookies_on_redirect() -> Result<(), Error> {
let testserver = TestServer::new(cookie_and_redirect);
let url = format!("http://localhost:{}/first", testserver.port);
let agent = Agent::default();
let agent = Agent::new();
agent.post(&url).call()?;
let cookies = agent.state.jar.get_request_cookies(
let cookies = agent.state.cookie_tin.get_request_cookies(
&format!("https://localhost:{}/", testserver.port)
.parse()
.unwrap(),
@@ -166,17 +164,17 @@ fn dirty_streams_not_returned() -> Result<(), Error> {
stream.write_all(b"\r\n")?;
stream.write_all(b"5\r\n")?;
stream.write_all(b"corgi\r\n")?;
stream.write_all(b"8\r\n")?;
stream.write_all(b"dachsund\r\n")?;
stream.write_all(b"9\r\n")?;
stream.write_all(b"dachshund\r\n")?;
stream.write_all(b"0\r\n")?;
stream.write_all(b"\r\n")?;
Ok(())
});
let url = format!("http://localhost:{}/", testserver.port);
let agent = Agent::default();
let agent = Agent::new();
let resp = agent.get(&url).call()?;
let resp_str = resp.into_string()?;
assert_eq!(resp_str, "corgidachsund");
assert_eq!(resp_str, "corgidachshund");
// Now fetch it again, but only read part of the body.
let resp_to_be_dropped = agent.get(&url).call()?;

View File

@@ -1,64 +0,0 @@
use crate::test;
use super::super::*;
#[test]
fn basic_auth() {
test::set_handler("/basic_auth", |unit| {
assert_eq!(
unit.header("Authorization").unwrap(),
"Basic bWFydGluOnJ1YmJlcm1hc2hndW0="
);
test::make_response(200, "OK", vec![], vec![])
});
let resp = get("test://host/basic_auth")
.auth("martin", "rubbermashgum")
.call()
.unwrap();
assert_eq!(resp.status(), 200);
}
#[test]
fn kind_auth() {
test::set_handler("/kind_auth", |unit| {
assert_eq!(unit.header("Authorization").unwrap(), "Digest abcdefgh123");
test::make_response(200, "OK", vec![], vec![])
});
let resp = get("test://host/kind_auth")
.auth_kind("Digest", "abcdefgh123")
.call()
.unwrap();
assert_eq!(resp.status(), 200);
}
#[test]
fn url_auth() {
test::set_handler("/url_auth", |unit| {
assert_eq!(
unit.header("Authorization").unwrap(),
"Basic QWxhZGRpbjpPcGVuU2VzYW1l"
);
test::make_response(200, "OK", vec![], vec![])
});
let resp = get("test://Aladdin:OpenSesame@host/url_auth")
.call()
.unwrap();
assert_eq!(resp.status(), 200);
}
#[test]
fn url_auth_overridden() {
test::set_handler("/url_auth_overridden", |unit| {
assert_eq!(
unit.header("Authorization").unwrap(),
"Basic bWFydGluOnJ1YmJlcm1hc2hndW0="
);
test::make_response(200, "OK", vec![], vec![])
});
let agent = AgentBuilder::new().auth("martin", "rubbermashgum").build();
let resp = agent
.get("test://Aladdin:OpenSesame@host/url_auth_overridden")
.call()
.unwrap();
assert_eq!(resp.status(), 200);
}

View File

@@ -7,7 +7,6 @@ use std::sync::{Arc, Mutex};
use std::{collections::HashMap, net::ToSocketAddrs};
mod agent_test;
mod auth;
mod body_read;
mod body_send;
mod query_string;

View File

@@ -1,11 +1,11 @@
#[cfg(any(feature = "tls", feature = "native-tls"))]
#[cfg(feature = "tls")]
use std::io::Read;
#[cfg(any(feature = "tls", feature = "native-tls"))]
#[cfg(feature = "tls")]
use super::super::*;
#[test]
#[cfg(any(feature = "tls", feature = "native-tls"))]
#[cfg(feature = "tls")]
fn read_range() {
let resp = get("https://ureq.s3.eu-central-1.amazonaws.com/sherlock.txt")
.set("Range", "bytes=1000-1999")

View File

@@ -29,7 +29,11 @@ fn redirect_many() {
test::set_handler("/redirect_many2", |_| {
test::make_response(302, "Go here", vec!["Location: /redirect_many3"], vec![])
});
let result = get("test://host/redirect_many1").redirects(1).call();
let result = builder()
.redirects(1)
.build()
.get("test://host/redirect_many1")
.call();
assert!(matches!(result, Err(Error::TooManyRedirects)));
}
@@ -38,7 +42,11 @@ fn redirect_off() -> Result<(), Error> {
test::set_handler("/redirect_off", |_| {
test::make_response(302, "Go here", vec!["Location: somewhere.else"], vec![])
});
let resp = get("test://host/redirect_off").redirects(0).call()?;
let resp = builder()
.redirects(0)
.build()
.get("test://host/redirect_off")
.call()?;
assert_eq!(resp.status(), 302);
assert!(resp.has("Location"));
assert_eq!(resp.header("Location").unwrap(), "somewhere.else");
@@ -96,7 +104,7 @@ fn redirect_host() {
Ok(())
});
let url = format!("http://localhost:{}/", srv.port);
let resp = crate::Agent::default().get(&url).call();
let resp = crate::Agent::new().get(&url).call();
let err = resp.err();
assert!(
matches!(err, Some(Error::DnsFailed(_))),

View File

@@ -130,8 +130,7 @@ fn request_debug() {
let req = get("http://localhost/my/page")
.set("Authorization", "abcdef")
.set("Content-Length", "1234")
.set("Content-Type", "application/json")
.build();
.set("Content-Type", "application/json");
let s = format!("{:?}", req);
@@ -143,8 +142,7 @@ fn request_debug() {
let req = get("http://localhost/my/page?q=z")
.query("foo", "bar baz")
.set("Authorization", "abcdef")
.build();
.set("Authorization", "abcdef");
let s = format!("{:?}", req);

View File

@@ -13,7 +13,7 @@ pub struct TestHeaders(Vec<String>);
impl TestHeaders {
// Return the path for a request, e.g. /foo from "GET /foo HTTP/1.1"
#[cfg(feature = "cookie")]
#[cfg(feature = "cookies")]
pub fn path(&self) -> &str {
if self.0.len() == 0 {
""
@@ -22,7 +22,7 @@ impl TestHeaders {
}
}
#[cfg(feature = "cookie")]
#[cfg(feature = "cookies")]
pub fn headers(&self) -> &[String] {
&self.0[1..]
}
@@ -57,12 +57,12 @@ impl TestServer {
eprintln!("testserver: handling just-accepted stream: {}", e);
break;
}
thread::spawn(move || handler(stream.unwrap()));
if done.load(Ordering::Relaxed) {
if done.load(Ordering::SeqCst) {
break;
} else {
thread::spawn(move || handler(stream.unwrap()));
}
}
println!("testserver on {} exiting", port);
});
TestServer {
port,
@@ -73,7 +73,7 @@ impl TestServer {
impl Drop for TestServer {
fn drop(&mut self) {
self.done.store(true, Ordering::Relaxed);
self.done.store(true, Ordering::SeqCst);
// Connect once to unblock the listen loop.
TcpStream::connect(format!("localhost:{}", self.port)).unwrap();
}

View File

@@ -25,9 +25,9 @@ fn dribble_body_respond(mut stream: TcpStream, contents: &[u8]) -> io::Result<()
}
fn get_and_expect_timeout(url: String) {
let agent = Agent::default();
let timeout = Duration::from_millis(500);
let resp = agent.get(&url).timeout(timeout).call().unwrap();
let agent = builder().timeout(timeout).build();
let resp = agent.get(&url).call().unwrap();
match resp.into_string() {
Err(io_error) => match io_error.kind() {
@@ -86,9 +86,9 @@ fn overall_timeout_reading_json() {
});
let url = format!("http://localhost:{}/", server.port);
let agent = Agent::default();
let timeout = Duration::from_millis(500);
let resp = agent.get(&url).timeout(timeout).call().unwrap();
let agent = builder().timeout(timeout).build();
let resp = agent.get(&url).call().unwrap();
match resp.into_json() {
Ok(_) => Err("successful response".to_string()),