diff --git a/Cargo.lock b/Cargo.lock index a708ff82..abedc7cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,6 +264,25 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +[[package]] +name = "hoot" +version = "0.1.2" +dependencies = [ + "httparse", + "log", +] + +[[package]] +name = "hootbin" +version = "0.1.0" +dependencies = [ + "fastrand", + "hoot", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "http" version = "0.2.11" @@ -286,6 +305,12 @@ dependencies = [ "itoa", ] +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + [[package]] name = "humantime" version = "2.1.0" @@ -450,18 +475,18 @@ checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -633,18 +658,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.188" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", @@ -653,9 +678,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -687,9 +712,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "2.0.37" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -718,6 +743,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.29" @@ -799,6 +844,7 @@ dependencies = [ "encoding_rs", "env_logger", "flate2", + "hootbin", "http 0.2.11", "http 1.0.0", "log", diff --git a/Cargo.toml b/Cargo.toml index e2e1d116..23c05122 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,9 @@ brotli-decompressor = { version = "2.3.2", optional = true } http-02 = { package = "http", version = "0.2", optional = true } http = { version = "1.0", optional = true } +# This can't be in dev-dependencies due to doc tests. +hootbin = { version = "0.1.0" } + [dev-dependencies] serde = { version = "1", features = ["derive"] } env_logger = "0.10" diff --git a/README.md b/README.md index 9c2e9a44..ff6eec34 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,8 @@ You can control them when including ureq as a dependency. does nothing for `native-tls`. * `gzip` enables requests of gzip-compressed responses and decompresses them. This is enabled by default. * `brotli` enables requests brotli-compressed responses and decompresses them. -* `http-interop` enables conversion methods to and from `http::Response` and `http::request::Builder`. +* `http-interop` enables conversion methods to and from `http::Response` and `http::request::Builder` (v0.2). +* `http` enables conversion methods to and from `http::Response` and `http::request::Builder` (v1.0). ## Plain requests diff --git a/src/agent.rs b/src/agent.rs index 986d3f92..202869ff 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -93,11 +93,11 @@ pub(crate) struct AgentConfig { /// let mut agent = ureq::agent(); /// /// agent -/// .post("http://example.com/login") +/// .post("http://example.com/post/login") /// .call()?; /// /// let secret = agent -/// .get("http://example.com/my-protected-page") +/// .get("http://example.com/get/my-protected-page") /// .call()? /// .into_string()?; /// @@ -173,7 +173,7 @@ impl Agent { /// let agent = ureq::agent(); /// /// let mut url: Url = "http://example.com/some-page".parse()?; - /// url.set_path("/robots.txt"); + /// url.set_path("/get/robots.txt"); /// let resp: Response = agent /// .request_url("GET", &url) /// .call()?; diff --git a/src/lib.rs b/src/lib.rs index 70d42e49..a99d1a0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,7 +104,7 @@ //! # 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") +//! let resp: String = ureq::post("http://myapi.example.com/post/ingest") //! .set("X-My-Header", "Secret") //! .send_json(ureq::json!({ //! "name": "martin", @@ -527,7 +527,7 @@ pub fn request(method: &str, path: &str) -> Request { /// let agent = ureq::agent(); /// /// let mut url: Url = "http://example.com/some-page".parse()?; -/// url.set_path("/robots.txt"); +/// url.set_path("/get/robots.txt"); /// let resp: ureq::Response = ureq::request_url("GET", &url) /// .call()?; /// # Ok(()) diff --git a/src/request.rs b/src/request.rs index f92ddde1..33c68eed 100644 --- a/src/request.rs +++ b/src/request.rs @@ -18,7 +18,7 @@ pub type Result = std::result::Result; /// ``` /// # fn main() -> Result<(), ureq::Error> { /// # ureq::is_test(true); -/// let response = ureq::get("http://example.com/form") +/// let response = ureq::get("http://example.com/get") /// .query("foo", "bar baz") // add ?foo=bar+baz /// .call()?; // run the request /// # Ok(()) diff --git a/src/response.rs b/src/response.rs index 4085cb0f..0223068a 100644 --- a/src/response.rs +++ b/src/response.rs @@ -201,8 +201,8 @@ impl Response { /// ``` /// # fn main() -> Result<(), ureq::Error> { /// # ureq::is_test(true); - /// let resp = ureq::get("http://example.com/").call()?; - /// assert!(matches!(resp.header("content-type"), Some("text/html; charset=ISO-8859-1"))); + /// let resp = ureq::get("http://example.com/charset/iso").call()?; + /// assert_eq!(resp.header("content-type"), Some("text/html; charset=ISO-8859-1")); /// assert_eq!("text/html", resp.content_type()); /// # Ok(()) /// # } @@ -225,8 +225,8 @@ impl Response { /// ``` /// # fn main() -> Result<(), ureq::Error> { /// # ureq::is_test(true); - /// let resp = ureq::get("http://example.com/").call()?; - /// assert!(matches!(resp.header("content-type"), Some("text/html; charset=ISO-8859-1"))); + /// let resp = ureq::get("http://example.com/charset/iso").call()?; + /// assert_eq!(resp.header("content-type"), Some("text/html; charset=ISO-8859-1")); /// assert_eq!("ISO-8859-1", resp.charset()); /// # Ok(()) /// # } @@ -500,15 +500,15 @@ impl Response { /// /// #[derive(Deserialize)] /// struct Message { - /// hello: String, + /// text: String, /// } /// /// let message: Message = - /// ureq::get("http://example.com/hello_world.json") + /// ureq::get("http://example.com/get/hello_world.json") /// .call()? /// .into_json()?; /// - /// assert_eq!(message.hello, "world"); + /// assert_eq!(message.text, "Ok"); /// # Ok(()) /// # } /// ``` @@ -520,11 +520,11 @@ impl Response { /// ``` /// # fn main() -> Result<(), ureq::Error> { /// # ureq::is_test(true); - /// let json: serde_json::Value = ureq::get("http://example.com/hello_world.json") + /// let json: serde_json::Value = ureq::get("http://example.com/get/hello_world.json") /// .call()? /// .into_json()?; /// - /// assert_eq!(json["hello"], "world"); + /// assert_eq!(json["text"], "Ok"); /// # Ok(()) /// # } /// ``` diff --git a/src/testserver.rs b/src/testserver.rs index 5df8f6b4..fd844efc 100644 --- a/src/testserver.rs +++ b/src/testserver.rs @@ -1,12 +1,10 @@ +use std::io; +use std::net::ToSocketAddrs; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::thread; use std::time::Duration; -use std::{ - io::{self, BufRead, BufReader, Write}, - net::ToSocketAddrs, -}; use crate::{Agent, AgentBuilder}; @@ -16,41 +14,23 @@ pub(crate) fn test_agent() -> Agent { #[cfg(test)] let _ = env_logger::try_init(); - let testserver = TestServer::new(|mut stream: TcpStream| -> io::Result<()> { - let headers = read_request(&stream); - if headers.0.is_empty() { - // no headers probably means it's the initial request to check test server is up. - } else if headers.path() == "/status/200" { - stream.write_all(b"HTTP/1.1 200 OK\r\n\r\n")?; - } else if headers.path() == "/status/500" { - stream.write_all(b"HTTP/1.1 500 Server Internal Error\r\n\r\n")?; - } else if headers.path() == "/bytes/100" { - stream.write_all(b"HTTP/1.1 200 OK\r\n")?; - stream.write_all(b"Content-Length: 100\r\n")?; - stream.write_all(b"\r\n")?; - stream.write_all(&[0; 100])?; - } else if headers.path() == "/hello_world.json" { - stream.write_all(b"HTTP/1.1 200 OK\r\n")?; - stream.write_all(b"\r\n")?; - stream.write_all(br#"{"hello": "world"}"#)?; - } else if headers.path() == "/status/301" { - stream.write_all(b"HTTP/1.1 301 Found\r\n")?; - stream.write_all(b"Location: /status/200\r\n")?; - stream.write_all(b"\r\n")?; - } else if headers.path() == "/status/307" { - stream.write_all(b"HTTP/1.1 307 Found\r\n")?; - stream.write_all(b"Location: /status/200\r\n")?; - stream.write_all(b"\r\n")?; - } else { - stream.write_all(b"HTTP/1.1 200 OK\r\n")?; - stream.write_all(b"Transfer-Encoding: chunked\r\n")?; - stream.write_all(b"Content-Type: text/html; charset=ISO-8859-1\r\n")?; - stream.write_all(b"\r\n")?; - stream.write_all(b"7\r\n")?; - stream.write_all(b"success\r\n")?; - stream.write_all(b"0\r\n")?; - stream.write_all(b"\r\n")?; - } + let testserver = TestServer::new(|stream: TcpStream| -> io::Result<()> { + use hootbin::serve_single; + let o = stream.try_clone().expect("TcpStream to be clonable"); + let i = stream; + match serve_single(i, o, "https://hootbin.test/") { + Ok(()) => {} + Err(e) => { + if let hootbin::Error::Io(ioe) = &e { + if ioe.kind() == io::ErrorKind::UnexpectedEof { + // accept this. the pre-connect below is always erroring. + return Ok(()); + } + } + + println!("TestServer error: {:?}", e); + } + }; Ok(()) }); // Slightly tricky thing here: we want to make sure the TestServer lives @@ -97,7 +77,10 @@ impl TestHeaders { // Read a stream until reaching a blank line, in order to consume // request headers. +#[cfg(test)] pub fn read_request(stream: &TcpStream) -> TestHeaders { + use std::io::{BufRead, BufReader}; + let mut results = vec![]; for line in BufReader::new(stream).lines() { match line {