Skip to content

Commit 8bcf81f

Browse files
committed
Add a hyper client implementation
Allows using this client facade with a `tokio` executor
1 parent f50f436 commit 8bcf81f

File tree

3 files changed

+185
-1
lines changed

3 files changed

+185
-1
lines changed

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ features = ["docs"]
1616
rustdoc-args = ["--cfg", "feature=\"docs\""]
1717

1818
[features]
19-
default = ["h1_client"]
19+
default = ["h1_client", "hyper_client"]
2020
docs = ["h1_client"]
2121
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" }
@@ -33,6 +34,10 @@ 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"] }

src/hyper.rs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
//! http-client implementation for reqwest
2+
use super::{Error, HttpClient, Request, Response};
3+
use http_types::StatusCode;
4+
use hyper::body::HttpBody;
5+
use hyper_tls::HttpsConnector;
6+
use std::convert::{TryFrom, TryInto};
7+
use std::str::FromStr;
8+
9+
/// Hyper-based HTTP Client.
10+
#[derive(Debug)]
11+
struct HyperClient {}
12+
13+
impl HttpClient for HyperClient {
14+
fn send(&self, req: Request) -> futures::future::BoxFuture<'static, Result<Response, Error>> {
15+
Box::pin(async move {
16+
let req = HyperHttpRequest::try_from(req).await?.into_inner();
17+
// UNWRAP: Scheme guaranteed to be "http" or "https" as part of conversion
18+
let scheme = req.uri().scheme_str().unwrap();
19+
20+
let response = match scheme {
21+
"http" => {
22+
let client = hyper::Client::builder().build_http::<hyper::Body>();
23+
client.request(req).await
24+
}
25+
"https" => {
26+
let https = HttpsConnector::new();
27+
let client = hyper::Client::builder().build::<_, hyper::Body>(https);
28+
client.request(req).await
29+
}
30+
_ => unreachable!(),
31+
}?;
32+
33+
let resp = HttpTypesResponse::try_from(response).await?.into_inner();
34+
Ok(resp)
35+
})
36+
}
37+
}
38+
39+
struct HyperHttpRequest {
40+
inner: hyper::Request<hyper::Body>,
41+
}
42+
43+
impl HyperHttpRequest {
44+
async fn try_from(mut value: http_types::Request) -> Result<Self, Error> {
45+
// Note: Much of this code was taken from the `http-types` compat implementation. Trying to
46+
// figure out the feature flags to conditionally compile with compat support was rather
47+
// difficult, so copying code was deemed a reasonable intermediate solution.
48+
// Also, because converting the `http_types` body to bytes is async, we can't implement `TryFrom`
49+
50+
// TODO: Do this without a `String` allocation
51+
let method = hyper::Method::from_str(&value.method().to_string()).unwrap();
52+
53+
let version = value
54+
.version()
55+
.map(|v| match v {
56+
http_types::Version::Http0_9 => Ok(hyper::Version::HTTP_09),
57+
http_types::Version::Http1_0 => Ok(hyper::Version::HTTP_10),
58+
http_types::Version::Http1_1 => Ok(hyper::Version::HTTP_11),
59+
http_types::Version::Http2_0 => Ok(hyper::Version::HTTP_2),
60+
http_types::Version::Http3_0 => Ok(hyper::Version::HTTP_3),
61+
_ => Err(Error::from_str(
62+
StatusCode::BadRequest,
63+
"unrecognized HTTP version",
64+
)),
65+
})
66+
.or(Some(Ok(hyper::Version::default())))
67+
.unwrap()?;
68+
69+
// UNWRAP: This unwrap is unjustified in `http-types`, need to check if it's actually safe.
70+
let uri = hyper::Uri::try_from(&format!("{}", value.url())).unwrap();
71+
72+
// `HttpClient` depends on the scheme being either "http" or "https"
73+
match uri.scheme_str() {
74+
Some("http") | Some("https") => (),
75+
_ => return Err(Error::from_str(StatusCode::BadRequest, "invalid scheme")),
76+
};
77+
78+
let mut request = hyper::Request::builder();
79+
80+
// UNWRAP: Default builder is safe
81+
let req_headers = request.headers_mut().unwrap();
82+
for (name, values) in &value {
83+
// UNWRAP: http-types and http have equivalent validation rules
84+
let name = hyper::header::HeaderName::from_str(name.as_str()).unwrap();
85+
86+
for value in values.iter() {
87+
// UNWRAP: http-types and http have equivalent validation rules
88+
let value =
89+
hyper::header::HeaderValue::from_bytes(value.as_str().as_bytes()).unwrap();
90+
req_headers.append(&name, value);
91+
}
92+
}
93+
94+
let body = value.body_bytes().await?;
95+
let body = hyper::Body::from(body);
96+
97+
let req = hyper::Request::builder()
98+
.method(method)
99+
.version(version)
100+
.uri(uri)
101+
.body(body)?;
102+
103+
Ok(HyperHttpRequest { inner: req })
104+
}
105+
106+
fn into_inner(self) -> hyper::Request<hyper::Body> {
107+
self.inner
108+
}
109+
}
110+
111+
struct HttpTypesResponse {
112+
inner: http_types::Response,
113+
}
114+
115+
impl HttpTypesResponse {
116+
117+
async fn try_from(value: hyper::Response<hyper::Body>) -> Result<Self, Error> {
118+
// Note: Much of this code was taken from the `http-types` compat implementation. Trying to
119+
// figure out the feature flags to conditionally compile with compat support was rather
120+
// difficult, so copying code was deemed a reasonable intermediate solution.
121+
let (parts, mut body) = value.into_parts();
122+
123+
// UNWRAP: http and http-types implement the same status codes
124+
let status: StatusCode = parts.status.as_u16().try_into().unwrap();
125+
126+
let version = match parts.version {
127+
hyper::Version::HTTP_09 => Ok(http_types::Version::Http0_9),
128+
hyper::Version::HTTP_10 => Ok(http_types::Version::Http1_0),
129+
hyper::Version::HTTP_11 => Ok(http_types::Version::Http1_1),
130+
hyper::Version::HTTP_2 => Ok(http_types::Version::Http2_0),
131+
hyper::Version::HTTP_3 => Ok(http_types::Version::Http3_0),
132+
// TODO: Is this realistically reachable, and should it be marked BadRequest?
133+
_ => Err(Error::from_str(
134+
StatusCode::BadRequest,
135+
"unrecognized HTTP response version",
136+
)),
137+
}?;
138+
139+
let body = match body.data().await {
140+
None => None,
141+
Some(Ok(b)) => Some(b),
142+
Some(Err(_)) => {
143+
return Err(Error::from_str(
144+
StatusCode::BadRequest,
145+
"unable to read HTTP response body",
146+
))
147+
}
148+
}
149+
.map(|b| http_types::Body::from_bytes(b.to_vec()))
150+
// TODO: How does `http-types` handle responses without bodies?
151+
.unwrap_or(http_types::Body::from_bytes(Vec::new()));
152+
153+
let mut res = Response::new(status);
154+
res.set_version(Some(version));
155+
156+
for (name, value) in parts.headers {
157+
// TODO: http_types uses an `unsafe` block here, should it be allowed for `hyper` as well?
158+
let value = value.as_bytes().to_owned();
159+
let value = http_types::headers::HeaderValue::from_bytes(value)?;
160+
161+
if let Some(name) = name {
162+
let name = name.as_str();
163+
let name = http_types::headers::HeaderName::from_str(name)?;
164+
res.insert_header(name, value);
165+
}
166+
}
167+
168+
res.set_body(body);
169+
Ok(HttpTypesResponse { inner: res })
170+
}
171+
172+
fn into_inner(self) -> http_types::Response {
173+
self.inner
174+
}
175+
}

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
use futures::future::BoxFuture;
1818

19+
#[cfg_attr(feature = "docs", doc(cfg(curl_client)))]
20+
#[cfg(all(feature = "hyper_client", not(target_arch = "wasm32")))]
21+
pub mod hyper;
22+
1923
#[cfg_attr(feature = "docs", doc(cfg(curl_client)))]
2024
#[cfg(all(feature = "curl_client", not(target_arch = "wasm32")))]
2125
pub mod isahc;

0 commit comments

Comments
 (0)