Skip to content

Commit a1043a5

Browse files
authored
Merge pull request #32 from bspeice/hyper
Add a `hyper` client implementation
2 parents ea2cff6 + 24f1b20 commit a1043a5

File tree

3 files changed

+200
-2
lines changed

3 files changed

+200
-2
lines changed

Cargo.toml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,22 @@ h1_client = ["async-h1", "async-std", "async-native-tls"]
2222
native_client = ["curl_client", "wasm_client"]
2323
curl_client = ["isahc", "async-std"]
2424
wasm_client = ["js-sys", "web-sys", "wasm-bindgen", "wasm-bindgen-futures"]
25+
hyper_client = ["hyper", "hyper-tls"]
2526

2627
[dependencies]
2728
futures = { version = "0.3.1" }
28-
http-types = "2.3.0"
29+
http-types = { version = "2.3.0", features = ["hyperium_http"] }
2930
log = "0.4.7"
3031

3132
# h1-client
3233
async-h1 = { version = "2.0.0", optional = true }
3334
async-std = { version = "1.6.0", default-features = false, optional = true }
3435
async-native-tls = { version = "0.3.1", optional = true }
3536

37+
# reqwest-client
38+
hyper = { version = "0.13.6", features = ["tcp"], optional = true }
39+
hyper-tls = { version = "0.4.3", optional = true }
40+
3641
# isahc-client
3742
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
3843
isahc = { version = "0.9", optional = true, default-features = false, features = ["http2"] }
@@ -63,5 +68,6 @@ features = [
6368

6469
[dev-dependencies]
6570
async-std = { version = "1.6.0", features = ["unstable", "attributes"] }
66-
tide = { version = "0.9.0" }
6771
portpicker = "0.1.0"
72+
tide = { version = "0.9.0" }
73+
tokio = { version = "0.2.21", features = ["macros"] }

src/hyper.rs

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
//! http-client implementation for reqwest
2+
3+
use super::{Error, HttpClient, Request, Response};
4+
use http_types::headers::{HeaderName, HeaderValue};
5+
use http_types::StatusCode;
6+
use hyper::body::HttpBody;
7+
use hyper_tls::HttpsConnector;
8+
use std::convert::TryFrom;
9+
use std::str::FromStr;
10+
11+
/// Hyper-based HTTP Client.
12+
#[derive(Debug)]
13+
pub struct HyperClient {}
14+
15+
impl HyperClient {
16+
/// Create a new client.
17+
///
18+
/// There is no specific benefit to reusing instances of this client.
19+
pub fn new() -> Self {
20+
HyperClient {}
21+
}
22+
}
23+
24+
impl HttpClient for HyperClient {
25+
fn send(&self, req: Request) -> futures::future::BoxFuture<'static, Result<Response, Error>> {
26+
Box::pin(async move {
27+
let req = HyperHttpRequest::try_from(req).await?.into_inner();
28+
// UNWRAP: Scheme guaranteed to be "http" or "https" as part of conversion
29+
let scheme = req.uri().scheme_str().unwrap();
30+
31+
let response = match scheme {
32+
"http" => {
33+
let client = hyper::Client::builder().build_http::<hyper::Body>();
34+
client.request(req).await
35+
}
36+
"https" => {
37+
let https = HttpsConnector::new();
38+
let client = hyper::Client::builder().build::<_, hyper::Body>(https);
39+
client.request(req).await
40+
}
41+
_ => unreachable!(),
42+
}?;
43+
44+
let resp = HttpTypesResponse::try_from(response).await?.into_inner();
45+
Ok(resp)
46+
})
47+
}
48+
}
49+
50+
struct HyperHttpRequest {
51+
inner: hyper::Request<hyper::Body>,
52+
}
53+
54+
impl HyperHttpRequest {
55+
async fn try_from(mut value: Request) -> Result<Self, Error> {
56+
// UNWRAP: This unwrap is unjustified in `http-types`, need to check if it's actually safe.
57+
let uri = hyper::Uri::try_from(&format!("{}", value.url())).unwrap();
58+
59+
// `HyperClient` depends on the scheme being either "http" or "https"
60+
match uri.scheme_str() {
61+
Some("http") | Some("https") => (),
62+
_ => return Err(Error::from_str(StatusCode::BadRequest, "invalid scheme")),
63+
};
64+
65+
let mut request = hyper::Request::builder();
66+
67+
// UNWRAP: Default builder is safe
68+
let req_headers = request.headers_mut().unwrap();
69+
for (name, values) in &value {
70+
// UNWRAP: http-types and http have equivalent validation rules
71+
let name = hyper::header::HeaderName::from_str(name.as_str()).unwrap();
72+
73+
for value in values.iter() {
74+
// UNWRAP: http-types and http have equivalent validation rules
75+
let value =
76+
hyper::header::HeaderValue::from_bytes(value.as_str().as_bytes()).unwrap();
77+
req_headers.append(&name, value);
78+
}
79+
}
80+
81+
let body = value.body_bytes().await?;
82+
let body = hyper::Body::from(body);
83+
84+
let request = request
85+
.method(value.method())
86+
.version(value.version().map(|v| v.into()).unwrap_or_default())
87+
.uri(uri)
88+
.body(body)?;
89+
90+
Ok(HyperHttpRequest { inner: request })
91+
}
92+
93+
fn into_inner(self) -> hyper::Request<hyper::Body> {
94+
self.inner
95+
}
96+
}
97+
98+
struct HttpTypesResponse {
99+
inner: Response,
100+
}
101+
102+
impl HttpTypesResponse {
103+
async fn try_from(value: hyper::Response<hyper::Body>) -> Result<Self, Error> {
104+
let (parts, mut body) = value.into_parts();
105+
106+
let body = match body.data().await {
107+
None => None,
108+
Some(Ok(b)) => Some(b),
109+
Some(Err(_)) => {
110+
return Err(Error::from_str(
111+
StatusCode::BadGateway,
112+
"unable to read HTTP response body",
113+
))
114+
}
115+
}
116+
.map(|b| http_types::Body::from_bytes(b.to_vec()))
117+
.unwrap_or(http_types::Body::empty());
118+
119+
let mut res = Response::new(parts.status);
120+
res.set_version(Some(parts.version.into()));
121+
122+
for (name, value) in parts.headers {
123+
let value = value.as_bytes().to_owned();
124+
let value = HeaderValue::from_bytes(value)?;
125+
126+
if let Some(name) = name {
127+
let name = name.as_str();
128+
let name = HeaderName::from_str(name)?;
129+
res.insert_header(name, value);
130+
}
131+
}
132+
133+
res.set_body(body);
134+
Ok(HttpTypesResponse { inner: res })
135+
}
136+
137+
fn into_inner(self) -> Response {
138+
self.inner
139+
}
140+
}
141+
142+
#[cfg(test)]
143+
mod tests {
144+
use crate::{Error, HttpClient};
145+
use http_types::{Method, Request, Url};
146+
use hyper::service::{make_service_fn, service_fn};
147+
use std::time::Duration;
148+
use tokio::sync::oneshot::channel;
149+
150+
use super::HyperClient;
151+
152+
async fn echo(
153+
req: hyper::Request<hyper::Body>,
154+
) -> Result<hyper::Response<hyper::Body>, hyper::Error> {
155+
Ok(hyper::Response::new(req.into_body()))
156+
}
157+
158+
#[tokio::test]
159+
async fn basic_functionality() {
160+
let (send, recv) = channel::<()>();
161+
162+
let recv = async move { recv.await.unwrap_or(()) };
163+
164+
let addr = ([127, 0, 0, 1], portpicker::pick_unused_port().unwrap()).into();
165+
let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(echo)) });
166+
let server = hyper::Server::bind(&addr)
167+
.serve(service)
168+
.with_graceful_shutdown(recv);
169+
170+
let client = HyperClient::new();
171+
let url = Url::parse(&format!("http://localhost:{}", addr.port())).unwrap();
172+
let mut req = Request::new(Method::Get, url);
173+
req.set_body("hello");
174+
175+
let client = async move {
176+
tokio::time::delay_for(Duration::from_millis(100)).await;
177+
let mut resp = client.send(req).await?;
178+
send.send(()).unwrap();
179+
assert_eq!(resp.body_string().await?, "hello");
180+
181+
Result::<(), Error>::Ok(())
182+
};
183+
184+
let (client_res, server_res) = tokio::join!(client, server);
185+
assert!(client_res.is_ok());
186+
assert!(server_res.is_ok());
187+
}
188+
}

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ pub mod native;
3232
#[cfg(feature = "h1_client")]
3333
pub mod h1;
3434

35+
#[cfg_attr(feature = "docs", doc(cfg(hyper_client)))]
36+
#[cfg(feature = "hyper_client")]
37+
pub mod hyper;
38+
3539
/// An HTTP Request type with a streaming body.
3640
pub type Request = http_types::Request;
3741

0 commit comments

Comments
 (0)