Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

7.0 - breaking: completely re-write feature flags #108

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
breaking: completely re-write feature flags
Completely overhauls the feature flags for this library.

Feature flags are now formatted with a dash `-` instead of an underscore.

No default features are selected.

The hyper feature is now versioned: `hyper0_14` (corresponding to hyper 0.14). This effectively brings in tokio 1.x support and drops support for 0.x.

The curl feature is now named `isahc0_9` (pending an upgrade to isahc 1.x).

The rustls version has been upgraded across the board to 0.20, and the `hyper0_14` client now has support for using rustls as the tls adaptor.

The `unstable-config` feature has been removed.

Doc updates have been made.
  • Loading branch information
Fishrock123 committed Jan 16, 2023
commit d2bd15b0b22f1d0e780bd9e2b2254ce83a792ee4
85 changes: 45 additions & 40 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -14,16 +14,21 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
backend: ["h1_client,native-tls", hyper_client, curl_client]
backend:
[
"h1-client,h1-rustls",
"hyper0_14-client,hyper0_14-rustls",
isahc0_9-client,
]

steps:
- uses: actions/checkout@master
- uses: actions/checkout@master

- name: check
run: cargo check --all-targets --workspace --no-default-features --features '${{ matrix.backend }}'
- name: check
run: cargo check --all-targets --workspace --no-default-features --features '${{ matrix.backend }}'

- name: tests
run: cargo test --all-targets --workspace --no-default-features --features '${{ matrix.backend }}'
- name: tests
run: cargo test --all-targets --workspace --no-default-features --features '${{ matrix.backend }}'

check_no_features:
name: Checking without default features
@@ -38,56 +43,56 @@ jobs:
name: Running clippy & fmt & docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/checkout@master

- name: Install nightly toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
override: true
components: clippy, rustfmt
- name: Install nightly toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
override: true
components: clippy, rustfmt

- name: clippy
run: cargo clippy --all-targets --workspace --features=docs
- name: clippy
run: cargo clippy --all-targets --workspace --features=docs

- name: fmt
run: cargo fmt --all -- --check
- name: fmt
run: cargo fmt --all -- --check

- name: docs
run: cargo doc --no-deps --features=docs
- name: docs
run: cargo doc --no-deps --features=docs

check_wasm:
name: Check wasm targets
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@master
- uses: actions/checkout@master

- name: Install nightly with wasm32-unknown-unknown
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
target: wasm32-unknown-unknown
override: true
- name: Install nightly with wasm32-unknown-unknown
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
target: wasm32-unknown-unknown
override: true

- name: check
uses: actions-rs/cargo@v1
with:
command: check
args: --target wasm32-unknown-unknown --no-default-features --features "native_client,wasm_client"
- name: check
uses: actions-rs/cargo@v1
with:
command: check
args: --target wasm32-unknown-unknown --no-default-features --features "native_client,wasm_client"

check_features:
name: Check feature combinations
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/checkout@master

- name: Install cargo-hack
run: cargo install cargo-hack
- name: Install cargo-hack
run: cargo install cargo-hack

- name: Check all feature combinations works properly
# * `--feature-powerset` - run for the feature powerset of the package
# * `--no-dev-deps` - build without dev-dependencies to avoid https://github.com/rust-lang/cargo/issues/4866
# * `--skip docs` - skip `docs` feature
run: cargo hack check --feature-powerset --no-dev-deps --skip docs
- name: Check all feature combinations works properly
# * `--feature-powerset` - run for the feature powerset of the package
# * `--no-dev-deps` - build without dev-dependencies to avoid https://github.com/rust-lang/cargo/issues/4866
# * `--skip docs` - skip `docs` feature
run: cargo hack check --feature-powerset --no-dev-deps --skip docs
62 changes: 37 additions & 25 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "http-client"
version = "6.5.3"
version = "7.0.0-alpha.1"
license = "MIT OR Apache-2.0"
repository = "https://github.com/http-rs/http-client"
documentation = "https://docs.rs/http-client"
@@ -20,55 +20,66 @@ features = ["docs"]
rustdoc-args = ["--cfg", "feature=\"docs\""]

[features]
default = ["h1_client", "native-tls"]
docs = ["h1_client", "curl_client", "wasm_client", "hyper_client"]
docs = ["h1-client", "isahc0_9-client", "wasm-client", "hyper0_14-client", "h1-rustls", "h1-native-tls"]

h1_client = ["async-h1", "async-std", "dashmap", "deadpool", "futures"]
native_client = ["curl_client", "wasm_client"]
curl_client = ["isahc", "async-std"]
wasm_client = ["js-sys", "web-sys", "wasm-bindgen", "wasm-bindgen-futures", "futures", "async-std"]
hyper_client = ["hyper", "hyper-tls", "http-types/hyperium_http", "futures-util", "tokio"]
h1-client = ["async-h1", "async-std", "dashmap", "deadpool", "futures"]
native-client = ["isahc0_9-client", "wasm-client"]
isahc0_9-client = ["isahc", "async-std"]
wasm-client = ["js-sys", "web-sys", "wasm-bindgen", "wasm-bindgen-futures", "futures", "async-std", "send_wrapper"]
hyper0_14-client = ["hyper0_14", "http-types/hyperium_http", "futures-util", "tokio1"]

native-tls = ["async-native-tls"]
rustls = ["async-tls", "rustls_crate"]
h1-rustls = ["async-rustls", "rustls_crate", "webpki-roots"]
h1-native-tls = ["async-native-tls"]

unstable-config = [] # deprecated
hyper0_14-rustls = ["hyper0_14-rustls-lib", "rustls_crate"]
hyper0_14-native-tls = ["hyper0_14-tls-lib"]

[dependencies]
async-trait = "0.1.37"
http-types = "2.3.0"
log = "0.4.7"
cfg-if = "1.0.0"

# h1_client
# h1-client
async-h1 = { version = "2.0.0", optional = true }
async-std = { version = "1.6.0", default-features = false, optional = true }
async-native-tls = { version = "0.3.1", optional = true }
dashmap = { version = "5.3.4", optional = true }
deadpool = { version = "0.7.0", optional = true }
futures = { version = "0.3.8", optional = true }

# h1_client_rustls
async-tls = { version = "0.11", optional = true }
rustls_crate = { version = "0.19", optional = true, package = "rustls" }
# h1-client + h1-rustls
# async-tls = { version = "0.11", optional = true }
async-rustls = { version = "0.3", optional = true }
webpki-roots = { version = "0.22.6", optional = true }

# h1-client + h1-native-tls
async-native-tls = { version = "0.3.1", optional = true }

# hyper0_14-client
hyper0_14 = { package = "hyper", version = "0.14", features = ["client", "http1", "tcp", "stream"], optional = true }
futures-util = { version = "0.3", features = ["io"], optional = true }
tokio1 = { package = "tokio", version = "1", features = ["time"], optional = true }

# hyper0_14-client + hyper0_14-rustls
hyper0_14-rustls-lib = { package = "hyper-rustls", version = "0.23", optional = true }

# hyper0_14-client + hyper0_14-native-tls
hyper0_14-tls-lib = { package = "hyper-tls", version = "0.5", optional = true }

# hyper_client
hyper = { version = "0.13.6", features = ["tcp"], optional = true }
hyper-tls = { version = "0.4.3", optional = true }
futures-util = { version = "0.3.5", features = ["io"], optional = true }
tokio = { version = "0.2", features = ["time"], optional = true }
# h1-rustls or hyper0_14-rustls
rustls_crate = { package = "rustls", version = "0.20", optional = true }

# curl_client
# isahc0_9-client
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
isahc = { version = "0.9", optional = true, default-features = false, features = ["http2"] }

# wasm_client
# wasm-client
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.25", optional = true }
wasm-bindgen = { version = "0.2.48", optional = true }
wasm-bindgen-futures = { version = "0.4.5", optional = true }
futures = { version = "0.3.1", optional = true }
send_wrapper = { version = "0.6.0", features = ["futures"] }
send_wrapper = { version = "0.6.0", features = ["futures"], optional = true }

[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys]
version = "0.3.25"
@@ -94,7 +105,8 @@ async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
portpicker = "0.1.0"
tide = { version = "0.15.0", default-features = false, features = ["h1-server"] }
tide-rustls = { version = "0.1.4" }
tokio = { version = "0.2.21", features = ["macros"] }
tokio1 = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
hyper0_14 = { package = "hyper", version = "0.14", features = ["server"] }
serde = "1.0"
serde_json = "1.0"
mockito = "0.23.3"
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -41,21 +41,37 @@
</h3>
</div>

## Installation
## Note on intent

With [cargo add][cargo-add] installed run:
This crate is designed to support developer-facing clients instead of being used directly.

```sh
$ cargo add http-client
```

[cargo-add]: https://github.com/killercup/cargo-edit
If you are looking for a Rust HTTP Client library which can support multiple backend http implementations,
consider using [Surf](https://crates.io/crates/surf), which depends on this library and provides a good developer experience.

## Safety

For non-wasm clients, this crate uses ``#![deny(unsafe_code)]`` to ensure everything is implemented in
100% Safe Rust.


## Feature Flags

This crate does not work without specifying feature flags. No features are set by default.

The following client backends are supported:
- [`async-h1`]() version 1.x, via the `h1-client` feature.
- [`hyper`]() version 0.14.x via the `hyper0_14-client` feature.
- libcurl through [`isahc`]() version 0.9.x via the `isahc0_9-client` feature.
- WASM to JavaScript `fetch` via the `wasm-client` feature.

Additionally TLS support can be enabled by the following options:
- `h1-rustls` uses [`rustls`](https://crates.io/crates/rustls) for the `h1-client`.
- `h1-native-tls` uses OpenSSL for the `h1-client` _(not recommended, no automated testing)_.
- `hyper0_14-rustls` uses [`rustls`](https://crates.io/crates/rustls) for the `hyper0-14-client`.
- `hyper0_14-native-tls` uses OpenSSL for the `hyper0_14-client` _(not recommended, no automated testing)_.
- `isahc0_9-client` (implicit support).
- `wasm-client` (implicit support).

## Contributing

Want to join us? Check out our ["Contributing" guide][contributing] and take a
60 changes: 52 additions & 8 deletions examples/print_client_debug.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,66 @@
#[cfg(any(
feature = "h1-client",
feature = "hyper0_14-client",
feature = "isahc0_9-client",
feature = "wasm-client"
))]
use http_client::HttpClient;
#[cfg(any(
feature = "h1-client",
feature = "hyper0_14-client",
feature = "isahc0_9-client",
feature = "wasm-client"
))]
use http_types::{Method, Request};

#[cfg(any(feature = "h1_client", feature = "docs"))]
#[cfg(feature = "hyper0_14-client")]
use tokio1 as tokio;

#[cfg(any(feature = "h1-client", feature = "docs"))]
use http_client::h1::H1Client as Client;
#[cfg(all(feature = "hyper_client", not(feature = "docs")))]
#[cfg(all(feature = "hyper0_14-client", not(feature = "docs")))]
use http_client::hyper::HyperClient as Client;
#[cfg(all(feature = "curl_client", not(feature = "docs")))]
#[cfg(all(feature = "isahc0_9-client", not(feature = "docs")))]
use http_client::isahc::IsahcClient as Client;
#[cfg(all(feature = "wasm_client", not(feature = "docs")))]
#[cfg(all(feature = "wasm-client", not(feature = "docs")))]
use http_client::wasm::WasmClient as Client;

#[async_std::main]
#[cfg(any(
feature = "h1-client",
feature = "hyper0_14-client",
feature = "isahc0_9-client",
feature = "wasm-client"
))]
#[cfg_attr(
any(
feature = "h1-client",
feature = "isahc0_9-client",
feature = "wasm-client"
),
async_std::main
)]
#[cfg_attr(feature = "hyper0_14-client", tokio::main)]
async fn main() {
let client = Client::new();

let req = Request::new(Method::Get, "http://example.org");
let mut args = std::env::args();
args.next(); // ignore binary name
let arg = args.next();
println!("{arg:?}");
let req = Request::new(Method::Get, arg.as_deref().unwrap_or("http://example.org"));

let response = client.send(req).await.unwrap();
dbg!(response);

client.send(req).await.unwrap();
dbg!(&client);
}

dbg!(client);
#[cfg(not(any(
feature = "h1-client",
feature = "hyper0_14-client",
feature = "isahc0_9-client",
feature = "wasm-client"
)))]
fn main() {
eprintln!("ERROR: A client backend must be select via `--features`: h1-client, hyper0_14-client, isahc0_9-client, wasm-client")
}
58 changes: 45 additions & 13 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -35,12 +35,30 @@ pub struct Config {
/// - `wasm_client`: No effect. Web browsers do not support such an option.
pub max_connections_per_host: usize,
/// TLS Configuration (Rustls)
#[cfg_attr(feature = "docs", doc(cfg(feature = "h1_client")))]
#[cfg(all(feature = "h1_client", feature = "rustls"))]
///
/// Available for the following backends:
/// - `h1-client` with `h1-rustls` feature.
/// - `hyper0_14-client` with `hyper0_14-rustls` feature.
///
/// Not available for curl or wasm clients.
#[cfg_attr(
feature = "docs",
doc(cfg(any(feature = "h1-rustls", feature = "hyper0_14-rustls")))
)]
#[cfg(any(feature = "h1-rustls", feature = "hyper0_14-rustls"))]
pub tls_config: Option<std::sync::Arc<rustls_crate::ClientConfig>>,
/// TLS Configuration (Native TLS)
#[cfg_attr(feature = "docs", doc(cfg(feature = "h1_client")))]
#[cfg(all(feature = "h1_client", feature = "native-tls", not(feature = "rustls")))]
///
/// Available for the following backends:
/// - `h1-client` with `h1-native-tls` feature.
///
/// Not available for curl or wasm clients.
/// Also not available for the hyper client.
#[cfg_attr(feature = "docs", doc(cfg(feature = "h1-native-tls")))]
#[cfg(all(
feature = "h1-native-tls",
not(any(feature = "h1-rustls", feature = "hyper0_14-rustls"))
))]
pub tls_config: Option<std::sync::Arc<async_native_tls::TlsConnector>>,
}

@@ -53,15 +71,19 @@ impl Debug for Config {
.field("timeout", &self.timeout)
.field("max_connections_per_host", &self.max_connections_per_host);

#[cfg(all(feature = "h1_client", feature = "rustls"))]
#[cfg(any(feature = "h1-rustls", feature = "hyper0_14-rustls"))]
{
if self.tls_config.is_some() {
dbg_struct.field("tls_config", &"Some(rustls::ClientConfig)");
dbg_struct.field("tls_config", &Some(format_args!("rustls::ClientConfig")));
} else {
dbg_struct.field("tls_config", &"None");
dbg_struct.field("tls_config", &None::<()>);
}
}
#[cfg(all(feature = "h1_client", feature = "native-tls", not(feature = "rustls")))]
#[cfg(all(
feature = "h1-client",
feature = "h1-native-tls",
not(feature = "h1-rustls")
))]
{
dbg_struct.field("tls_config", &self.tls_config);
}
@@ -78,7 +100,11 @@ impl Config {
tcp_no_delay: false,
timeout: Some(Duration::from_secs(60)),
max_connections_per_host: 50,
#[cfg(all(feature = "h1_client", any(feature = "rustls", feature = "native-tls")))]
#[cfg(any(
feature = "h1-rustls",
feature = "h1-native-tls",
feature = "hyper0_14-rustls",
))]
tls_config: None,
}
}
@@ -116,8 +142,11 @@ impl Config {
}

/// Set TLS Configuration (Rustls)
#[cfg_attr(feature = "docs", doc(cfg(feature = "h1_client")))]
#[cfg(all(feature = "h1_client", feature = "rustls"))]
#[cfg_attr(
feature = "docs",
doc(cfg(any(feature = "h1-rustls", feature = "hyper0_14-rustls")))
)]
#[cfg(any(feature = "h1-rustls", feature = "hyper0_14-rustls"))]
pub fn set_tls_config(
mut self,
tls_config: Option<std::sync::Arc<rustls_crate::ClientConfig>>,
@@ -126,8 +155,11 @@ impl Config {
self
}
/// Set TLS Configuration (Native TLS)
#[cfg_attr(feature = "docs", doc(cfg(feature = "h1_client")))]
#[cfg(all(feature = "h1_client", feature = "native-tls", not(feature = "rustls")))]
#[cfg_attr(feature = "docs", doc(cfg(feature = "h1-native-tls")))]
#[cfg(all(
feature = "h1-native-tls",
not(any(feature = "h1-rustls", feature = "hyper0_14-rustls"))
))]
pub fn set_tls_config(
mut self,
tls_config: Option<std::sync::Arc<async_native_tls::TlsConnector>>,
84 changes: 35 additions & 49 deletions src/h1/mod.rs
Original file line number Diff line number Diff line change
@@ -12,9 +12,9 @@ use deadpool::managed::Pool;
use http_types::StatusCode;

cfg_if::cfg_if! {
if #[cfg(feature = "rustls")] {
use async_tls::client::TlsStream;
} else if #[cfg(feature = "native-tls")] {
if #[cfg(feature = "h1-rustls")] {
use async_rustls::client::TlsStream;
} else if #[cfg(feature = "h1-native-tls")] {
use async_native_tls::TlsStream;
}
}
@@ -24,28 +24,28 @@ use crate::Config;
use super::{async_trait, Error, HttpClient, Request, Response};

mod tcp;
#[cfg(any(feature = "native-tls", feature = "rustls"))]
#[cfg(any(feature = "h1-native-tls", feature = "h1-rustls"))]
mod tls;

use tcp::{TcpConnWrapper, TcpConnection};
#[cfg(any(feature = "native-tls", feature = "rustls"))]
#[cfg(any(feature = "h1-native-tls", feature = "h1-rustls"))]
use tls::{TlsConnWrapper, TlsConnection};

type HttpPool = DashMap<SocketAddr, Pool<TcpStream, std::io::Error>>;
#[cfg(any(feature = "native-tls", feature = "rustls"))]
#[cfg(any(feature = "h1-native-tls", feature = "h1-rustls"))]
type HttpsPool = DashMap<SocketAddr, Pool<TlsStream<TcpStream>, Error>>;

/// async-h1 based HTTP Client, with connection pooling ("Keep-Alive").
pub struct H1Client {
http_pools: HttpPool,
#[cfg(any(feature = "native-tls", feature = "rustls"))]
#[cfg(any(feature = "h1-native-tls", feature = "h1-rustls"))]
https_pools: HttpsPool,
config: Arc<Config>,
}

impl Debug for H1Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let https_pools = if cfg!(any(feature = "native-tls", feature = "rustls")) {
let https_pools = if cfg!(any(feature = "h1-native-tls", feature = "h1-rustls")) {
self.http_pools
.iter()
.map(|pool| {
@@ -92,33 +92,11 @@ impl H1Client {
pub fn new() -> Self {
Self {
http_pools: DashMap::new(),
#[cfg(any(feature = "native-tls", feature = "rustls"))]
#[cfg(any(feature = "h1-native-tls", feature = "h1-rustls"))]
https_pools: DashMap::new(),
config: Arc::new(Config::default()),
}
}

/// Create a new instance.
#[deprecated(
since = "6.5.0",
note = "This function is misnamed. Prefer `Config::max_connections_per_host` instead."
)]
pub fn with_max_connections(max: usize) -> Self {
#[cfg(features = "h1_client")]
assert!(max > 0, "max_connections_per_host with h1_client must be greater than zero or it will deadlock!");

let config = Config {
max_connections_per_host: max,
..Default::default()
};

Self {
http_pools: DashMap::new(),
#[cfg(any(feature = "native-tls", feature = "rustls"))]
https_pools: DashMap::new(),
config: Arc::new(config),
}
}
}

#[async_trait]
@@ -127,26 +105,32 @@ impl HttpClient for H1Client {
req.insert_header("Connection", "keep-alive");

// Insert host
#[cfg(any(feature = "native-tls", feature = "rustls"))]
#[cfg(any(feature = "h1-native-tls", feature = "h1-rustls"))]
let host = req
.url()
.host_str()
.ok_or_else(|| Error::from_str(StatusCode::BadRequest, "missing hostname"))?
.to_string();

let scheme = req.url().scheme();
if scheme != "http"
&& (scheme != "https" || cfg!(not(any(feature = "native-tls", feature = "rustls"))))
{

if scheme == "https" {
if cfg!(not(any(feature = "h1-native-tls", feature = "h1-rustls"))) {
return Err(Error::from_str(
StatusCode::BadRequest,
"invalid url scheme `https` - requires `http-client` feature `h1-rustls` or `h1-native-tls`"
));
}
} else if scheme != "http" {
return Err(Error::from_str(
StatusCode::BadRequest,
format!("invalid url scheme '{}'", scheme),
format!("invalid url scheme `{scheme}`"),
));
}

let addrs = req.url().socket_addrs(|| match req.url().scheme() {
"http" => Some(80),
#[cfg(any(feature = "native-tls", feature = "rustls"))]
#[cfg(any(feature = "h1-native-tls", feature = "h1-rustls"))]
"https" => Some(443),
_ => None,
})?;
@@ -170,7 +154,7 @@ impl HttpClient for H1Client {
tcp_conn.await
};
}
#[cfg(any(feature = "native-tls", feature = "rustls"))]
#[cfg(any(feature = "h1-native-tls", feature = "h1-rustls"))]
"https" => {
let raw_stream = async_std::net::TcpStream::connect(addr).await?;
req.set_peer_addr(raw_stream.peer_addr().ok());
@@ -221,7 +205,7 @@ impl HttpClient for H1Client {
tcp_conn.await
};
}
#[cfg(any(feature = "native-tls", feature = "rustls"))]
#[cfg(any(feature = "h1-native-tls", feature = "h1-rustls"))]
"https" => {
let pool_ref = if let Some(pool_ref) = self.https_pools.get(&addr) {
pool_ref
@@ -245,8 +229,8 @@ impl HttpClient for H1Client {
Err(e) => return Err(Error::from_str(400, e.to_string())),
};

req.set_peer_addr(stream.get_ref().peer_addr().ok());
req.set_local_addr(stream.get_ref().local_addr().ok());
req.set_peer_addr(stream.get_ref().0.peer_addr().ok());
req.set_local_addr(stream.get_ref().0.local_addr().ok());

let tls_conn = client::connect(TlsConnWrapper::new(stream), req);
return if let Some(timeout) = self.config.timeout {
@@ -269,8 +253,8 @@ impl HttpClient for H1Client {
///
/// Config options may not impact existing connections.
fn set_config(&mut self, config: Config) -> http_types::Result<()> {
#[cfg(features = "h1_client")]
assert!(config.max_connections_per_host > 0, "max_connections_per_host with h1_client must be greater than zero or it will deadlock!");
#[cfg(feature = "h1-client")]
assert!(config.max_connections_per_host > 0, "max_connections_per_host with h1-client must be greater than zero or it will deadlock!");

self.config = Arc::new(config);

@@ -279,20 +263,20 @@ impl HttpClient for H1Client {

/// Get the current configuration.
fn config(&self) -> &Config {
&*self.config
&self.config
}
}

impl TryFrom<Config> for H1Client {
type Error = Infallible;

fn try_from(config: Config) -> Result<Self, Self::Error> {
#[cfg(features = "h1_client")]
assert!(config.max_connections_per_host > 0, "max_connections_per_host with h1_client must be greater than zero or it will deadlock!");
#[cfg(feature = "h1-client")]
assert!(config.max_connections_per_host > 0, "max_connections_per_host with h1-client must be greater than zero or it will deadlock!");

Ok(Self {
http_pools: DashMap::new(),
#[cfg(any(feature = "native-tls", feature = "rustls"))]
#[cfg(any(feature = "h1-native-tls", feature = "h1-rustls"))]
https_pools: DashMap::new(),
config: Arc::new(config),
})
@@ -301,12 +285,14 @@ impl TryFrom<Config> for H1Client {

#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;

use async_std::prelude::*;
use async_std::task;
use http_types::url::Url;
use http_types::Result;
use std::time::Duration;

use super::*;

fn build_test_request(url: Url) -> Request {
let mut req = Request::new(http_types::Method::Post, url);
2 changes: 1 addition & 1 deletion src/h1/tcp.rs
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ use futures::task::{Context, Poll};
use crate::Config;

#[derive(Clone)]
#[cfg_attr(not(feature = "rustls"), derive(std::fmt::Debug))]
#[cfg_attr(not(feature = "h1-rustls"), derive(std::fmt::Debug))]
pub(crate) struct TcpConnection {
addr: SocketAddr,
config: Arc<Config>,
83 changes: 54 additions & 29 deletions src/h1/tls.rs
Original file line number Diff line number Diff line change
@@ -9,17 +9,20 @@ use futures::io::{AsyncRead, AsyncWrite};
use futures::task::{Context, Poll};

cfg_if::cfg_if! {
if #[cfg(feature = "rustls")] {
use async_tls::client::TlsStream;
} else if #[cfg(feature = "native-tls")] {
if #[cfg(feature = "h1-rustls")] {
use std::convert::TryInto;
use std::io;

use async_rustls::client::TlsStream;
} else if #[cfg(feature = "h1-native-tls")] {
use async_native_tls::TlsStream;
}
}

use crate::{Config, Error};

#[derive(Clone)]
#[cfg_attr(not(feature = "rustls"), derive(std::fmt::Debug))]
#[cfg_attr(not(feature = "h1-rustls"), derive(std::fmt::Debug))]
pub(crate) struct TlsConnection {
host: String,
addr: SocketAddr,
@@ -84,7 +87,7 @@ impl Manager<TlsStream<TcpStream>, Error> for TlsConnection {
let mut buf = [0; 4];
let mut cx = Context::from_waker(futures::task::noop_waker_ref());

conn.get_ref()
conn.get_ref().0
.set_nodelay(self.config.tcp_no_delay)
.map_err(Error::from)?;

@@ -102,28 +105,50 @@ impl Manager<TlsStream<TcpStream>, Error> for TlsConnection {
}
}

cfg_if::cfg_if! {
if #[cfg(feature = "rustls")] {
#[allow(unused_variables)]
pub(crate) async fn add_tls(host: &str, stream: TcpStream, config: &Config) -> Result<TlsStream<TcpStream>, std::io::Error> {
let connector = if let Some(tls_config) = config.tls_config.as_ref().cloned() {
tls_config.into()
} else {
async_tls::TlsConnector::default()
};

connector.connect(host, stream).await
}
} else if #[cfg(feature = "native-tls")] {
#[allow(unused_variables)]
pub(crate) async fn add_tls(
host: &str,
stream: TcpStream,
config: &Config,
) -> Result<TlsStream<TcpStream>, async_native_tls::Error> {
let connector = config.tls_config.as_ref().cloned().unwrap_or_default();

connector.connect(host, stream).await
}
}
#[cfg(feature = "h1-rustls")]
#[allow(unused_variables)]
pub(crate) async fn add_tls(
host: &str,
stream: TcpStream,
config: &Config,
) -> Result<TlsStream<TcpStream>, io::Error> {
let connector: async_rustls::TlsConnector = if let Some(tls_config) =
config.tls_config.as_ref().cloned()
{
tls_config.into()
} else {
let mut root_certs = rustls_crate::RootCertStore::empty();
root_certs.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(|ta| {
rustls_crate::OwnedTrustAnchor::from_subject_spki_name_constraints(
ta.subject,
ta.spki,
ta.name_constraints,
)
}));
let config = rustls_crate::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_certs)
.with_no_client_auth();
Arc::new(config).into()
};

connector
.connect(
host.try_into()
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?,
stream,
)
.await
}

#[cfg(all(feature = "h1-native-tls", not(feature = "h1-rustls")))]
#[allow(unused_variables)]
pub(crate) async fn add_tls(
host: &str,
stream: TcpStream,
config: &Config,
) -> Result<TlsStream<TcpStream>, async_native_tls::Error> {
let connector = config.tls_config.as_ref().cloned().unwrap_or_default();

connector.connect(host, stream).await
}
113 changes: 100 additions & 13 deletions src/hyper.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! http-client implementation for reqwest
//! http-client implementation for hyper / tokio
use std::convert::{Infallible, TryFrom};
use std::fmt::Debug;
@@ -8,9 +8,16 @@ use std::str::FromStr;
use futures_util::stream::TryStreamExt;
use http_types::headers::{HeaderName, HeaderValue};
use http_types::StatusCode;
use hyper::body::HttpBody;
use hyper::client::connect::Connect;
use hyper_tls::HttpsConnector;
use hyper0_14 as hyper;
use hyper0_14::body::HttpBody;
use hyper0_14::client::connect::Connect;
use hyper0_14::client::HttpConnector;
use tokio1 as tokio;

#[cfg(feature = "hyper0_14-rustls")]
use hyper0_14_rustls_lib::HttpsConnectorBuilder;
#[cfg(feature = "hyper0_14-native-tls")]
use hyper0_14_tls_lib::HttpsConnector;

use crate::Config;

@@ -39,8 +46,21 @@ pub struct HyperClient {
impl HyperClient {
/// Create a new client instance.
pub fn new() -> Self {
let https = HttpsConnector::new();
let client = hyper::Client::builder().build(https);
#[allow(unused_mut)]
let mut connector = HttpConnector::new();
#[cfg(any(feature = "hyper0_14-rustls", feature = "hyper0_14-native-tls"))]
connector.enforce_http(false);

#[cfg(feature = "hyper0_14-native-tls")]
let connector = HttpsConnector::new_with_connector(connector);
#[cfg(feature = "hyper0_14-rustls")]
let connector = HttpsConnectorBuilder::default()
.with_native_roots()
.https_or_http()
.enable_http1()
.wrap_connector(connector);

let client = hyper::Client::builder().build(connector);

Self {
client: Box::new(client),
@@ -90,7 +110,27 @@ impl HttpClient for HyperClient {
///
/// Config options may not impact existing connections.
fn set_config(&mut self, config: Config) -> http_types::Result<()> {
let connector = HttpsConnector::new();
#[allow(unused_mut)]
let mut connector = HttpConnector::new();
#[cfg(any(feature = "hyper0_14-rustls", feature = "hyper0_14-native-tls"))]
connector.enforce_http(false);

#[cfg(feature = "hyper0_14-native-tls")]
let connector = HttpsConnector::new_with_connector(connector);
#[cfg(feature = "hyper0_14-rustls")]
let connector = match config.tls_config {
Some(ref config) => HttpsConnectorBuilder::default()
.with_tls_config(config.as_ref().clone())
.https_or_http()
.enable_http1()
.wrap_connector(connector),
None => HttpsConnectorBuilder::default()
.with_native_roots()
.https_or_http()
.enable_http1()
.wrap_connector(connector),
};

let mut builder = hyper::Client::builder();

if !config.http_keep_alive {
@@ -113,7 +153,27 @@ impl TryFrom<Config> for HyperClient {
type Error = Infallible;

fn try_from(config: Config) -> Result<Self, Self::Error> {
let connector = HttpsConnector::new();
#[allow(unused_mut)]
let mut connector = HttpConnector::new();
#[cfg(any(feature = "hyper0_14-rustls", feature = "hyper0_14-native-tls"))]
connector.enforce_http(false);

#[cfg(feature = "hyper0_14-native-tls")]
let connector = HttpsConnector::new_with_connector(connector);
#[cfg(feature = "hyper0_14-rustls")]
let connector = match config.tls_config {
Some(ref config) => HttpsConnectorBuilder::default()
.with_tls_config(config.as_ref().clone())
.https_or_http()
.enable_http1()
.wrap_connector(connector),
None => HttpsConnectorBuilder::default()
.with_native_roots()
.https_or_http()
.enable_http1()
.wrap_connector(connector),
};

let mut builder = hyper::Client::builder();

if !config.http_keep_alive {
@@ -136,8 +196,29 @@ impl HyperHttpRequest {

// `HyperClient` depends on the scheme being either "http" or "https"
match uri.scheme_str() {
#[cfg(not(any(feature = "hyper0_14-rustls", feature = "hyper0_14-native-tls")))]
Some("http") => (),
#[cfg(not(any(feature = "hyper0_14-rustls", feature = "hyper0_14-native-tls")))]
Some("https") => {
return Err(Error::from_str(
StatusCode::BadRequest,
"invalid url scheme `https` - requires `http-client` feature `hyper0_14-rustls` or `hyper0_14-native-tls`",
))
},
#[cfg(any(feature = "hyper0_14-rustls", feature = "hyper0_14-native-tls"))]
Some("http") | Some("https") => (),
_ => return Err(Error::from_str(StatusCode::BadRequest, "invalid scheme")),
Some(scheme) => {
return Err(Error::from_str(
StatusCode::BadRequest,
format!("invalid url scheme `{scheme}`"),
))
}
None => {
return Err(Error::from_str(
StatusCode::BadRequest,
format!("missing url scheme"),
))
}
};

let mut request = hyper::Request::builder();
@@ -180,7 +261,9 @@ impl HttpTypesResponse {
let (parts, body) = value.into_parts();

let size_hint = body.size_hint().upper().map(|s| s as usize);
let body = body.map_err(|err| io::Error::new(io::ErrorKind::Other, err.to_string()));
let body = TryStreamExt::map_err(body, |err| {
io::Error::new(io::ErrorKind::Other, err.to_string())
});
let body = http_types::Body::from_reader(body.into_async_read(), size_hint);

let mut res = Response::new(parts.status);
@@ -208,13 +291,17 @@ impl HttpTypesResponse {

#[cfg(test)]
mod tests {
use crate::{Error, HttpClient};
use std::time::Duration;

use hyper0_14 as hyper;
use tokio1 as tokio;

use http_types::{Method, Request, Url};
use hyper::service::{make_service_fn, service_fn};
use std::time::Duration;
use tokio::sync::oneshot::channel;

use super::HyperClient;
use crate::{Error, HttpClient};

async fn echo(
req: hyper::Request<hyper::Body>,
@@ -240,7 +327,7 @@ mod tests {
req.set_body("hello");

let client = async move {
tokio::time::delay_for(Duration::from_millis(100)).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let mut resp = client.send(req).await?;
send.send(()).unwrap();
assert_eq!(resp.body_string().await?, "hello");
6 changes: 4 additions & 2 deletions src/isahc.rs
Original file line number Diff line number Diff line change
@@ -127,12 +127,14 @@ impl TryFrom<Config> for IsahcClient {

#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;

use async_std::prelude::*;
use async_std::task;
use http_types::url::Url;
use http_types::Result;
use std::time::Duration;

use super::*;

fn build_test_request(url: Url) -> Request {
let mut req = Request::new(http_types::Method::Post, url);
25 changes: 12 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -6,36 +6,35 @@
#![forbid(future_incompatible, rust_2018_idioms)]
#![deny(missing_debug_implementations, nonstandard_style)]
#![warn(missing_docs, missing_doc_code_examples, unreachable_pub)]
#![warn(missing_docs, unreachable_pub)]
#![cfg_attr(feature = "docs", feature(doc_cfg))]
// Forbid `unsafe` for the native & curl features, but allow it (for now) under the WASM backend
#![cfg_attr(
not(all(feature = "wasm_client", target_arch = "wasm32")),
not(all(feature = "wasm-client", target_arch = "wasm32")),
forbid(unsafe_code)
)]

mod config;
pub use config::Config;

#[cfg_attr(feature = "docs", doc(cfg(feature = "curl_client")))]
#[cfg(all(feature = "curl_client", not(target_arch = "wasm32")))]
#[cfg_attr(feature = "docs", doc(cfg(feature = "isahc0_9-client")))]
#[cfg(all(feature = "isahc0_9-client", not(target_arch = "wasm32")))]
pub mod isahc;

#[cfg_attr(feature = "docs", doc(cfg(feature = "wasm_client")))]
#[cfg(all(feature = "wasm_client", target_arch = "wasm32"))]
#[cfg_attr(feature = "docs", doc(cfg(feature = "wasm-client")))]
#[cfg(all(feature = "wasm-client", target_arch = "wasm32"))]
pub mod wasm;

#[cfg_attr(feature = "docs", doc(cfg(feature = "native_client")))]
#[cfg(any(feature = "curl_client", feature = "wasm_client"))]
#[cfg_attr(feature = "docs", doc(cfg(feature = "native-client")))]
#[cfg(any(feature = "isahc0_9-client", feature = "wasm-client"))]
pub mod native;

#[cfg_attr(feature = "docs", doc(cfg(feature = "h1_client")))]
#[cfg_attr(feature = "docs", doc(cfg(feature = "default")))]
#[cfg(any(feature = "h1_client", feature = "h1_client_rustls"))]
#[cfg_attr(feature = "docs", doc(cfg(feature = "h1-client")))]
#[cfg(feature = "h1-client")]
pub mod h1;

#[cfg_attr(feature = "docs", doc(cfg(feature = "hyper_client")))]
#[cfg(feature = "hyper_client")]
#[cfg_attr(feature = "docs", doc(cfg(feature = "hyper0_14-client")))]
#[cfg(feature = "hyper0_14-client")]
pub mod hyper;

/// An HTTP Request type with a streaming body.
4 changes: 2 additions & 2 deletions src/native.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! http-client implementation for curl + fetch
#[cfg(all(feature = "curl_client", not(target_arch = "wasm32")))]
#[cfg(all(feature = "isahc0_9-client", not(target_arch = "wasm32")))]
pub use super::isahc::IsahcClient as NativeClient;

#[cfg(all(feature = "wasm_client", target_arch = "wasm32"))]
#[cfg(all(feature = "wasm-client", target_arch = "wasm32"))]
pub use super::wasm::WasmClient as NativeClient;
6 changes: 3 additions & 3 deletions src/wasm.rs
Original file line number Diff line number Diff line change
@@ -97,14 +97,14 @@ where
}

mod fetch {
use std::iter::{IntoIterator, Iterator};
use std::pin::Pin;

use js_sys::{Array, ArrayBuffer, Reflect, Uint8Array};
use wasm_bindgen::{prelude::*, JsCast};
use wasm_bindgen_futures::JsFuture;
use web_sys::{RequestInit, Window, WorkerGlobalScope};

use std::iter::{IntoIterator, Iterator};
use std::pin::Pin;

use http_types::StatusCode;

use crate::Error;
30 changes: 21 additions & 9 deletions tests/test.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
use cfg_if::cfg_if;

cfg_if! {
if #[cfg(any(
all(feature = "h1-client", feature = "h1-rustls"),
all(feature = "hyper0_14-client", feature = "hyper0_14-rustls"),
feature = "isahc0_9-client",
feature = "wasm-client"
))] {

use mockito::mock;

use http_client::HttpClient;
use http_types::{Body, Request, Response, Url};

use cfg_if::cfg_if;

cfg_if! {
if #[cfg(not(feature = "hyper_client"))] {
use async_std::test as atest;
} else {
if #[cfg(feature = "hyper0_14-client")] {
use tokio1 as tokio;
use tokio::test as atest;
} else {
use async_std::test as atest;
}
}

cfg_if! {
if #[cfg(feature = "curl_client")] {
if #[cfg(feature = "isahc0_9-client")] {
use http_client::isahc::IsahcClient as DefaultClient;
} else if #[cfg(feature = "wasm_client")] {
} else if #[cfg(feature = "wasm-client")] {
use http_client::wasm::WasmClient as DefaultClient;
} else if #[cfg(any(feature = "h1_client", feature = "h1_client_rustls"))] {
} else if #[cfg(feature = "h1-client")] {
use http_client::h1::H1Client as DefaultClient;
} else if #[cfg(feature = "hyper_client")] {
} else if #[cfg(feature = "hyper0_14-client")] {
use http_client::hyper::HyperClient as DefaultClient;
}
}
@@ -165,3 +174,6 @@ async fn fallback_to_ipv4() {
let req = Request::new(http_types::Method::Get, Url::parse(url).unwrap());
client.send(req.clone()).await.unwrap();
}

}
}