Skip to content

Commit 27987ed

Browse files
authored
Merge pull request #22 from dignifiedquire/h1
feat: implement client for async-h1
2 parents 98359bf + fe36283 commit 27987ed

File tree

4 files changed

+115
-105
lines changed

4 files changed

+115
-105
lines changed

Cargo.toml

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ documentation = "https://docs.rs/http-client"
77
description = "Types and traits for http clients."
88
keywords = ["http", "service", "client", "futures", "async"]
99
categories = ["asynchronous", "web-programming", "web-programming::http-client", "web-programming::websocket"]
10-
authors = ["Yoshua Wuyts <[email protected]>"]
10+
authors = ["Yoshua Wuyts <[email protected]>", "dignifiedquire <[email protected]>"]
1111
readme = "README.md"
1212
edition = "2018"
1313

@@ -16,18 +16,26 @@ features = ["docs"]
1616
rustdoc-args = ["--cfg", "feature=\"docs\""]
1717

1818
[features]
19-
docs = ["native_client"]
19+
default = ["h1_client"]
20+
docs = ["h1_client"]
21+
h1_client = ["async-h1", "async-std", "async-native-tls"]
2022
native_client = ["curl_client", "wasm_client"]
21-
curl_client = ["isahc"]
23+
curl_client = ["isahc", "async-std"]
2224
wasm_client = ["js-sys", "web-sys", "wasm-bindgen", "wasm-bindgen-futures"]
2325

2426
[dependencies]
2527
futures = { version = "0.3.1", features = ["compat", "io-compat"] }
26-
http = "0.1.19"
28+
http-types = { version = "1.0.1", features = ["hyperium_http"] }
29+
log = "0.4.7"
30+
31+
# h1-client
32+
async-h1 = { version = "1.0.0", optional = true }
33+
async-std = { version = "1.4.0", default-features = false, optional = true }
34+
async-native-tls = { version = "0.3.1", optional = true }
2735

2836
# isahc-client
2937
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
30-
isahc = { version = "0.8", optional = true, default-features = false, features = ["http2"] }
38+
isahc = { version = "0.9", optional = true, default-features = false, features = ["http2"] }
3139

3240
# wasm-client
3341
[target.'cfg(target_arch = "wasm32")'.dependencies]

src/h1.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//! http-client implementation for async-h1.
2+
3+
use super::{HttpClient, Request, Response};
4+
5+
use async_h1::client;
6+
use futures::future::BoxFuture;
7+
use http_types::{Error, StatusCode};
8+
9+
/// Async-h1 based HTTP Client.
10+
#[derive(Debug)]
11+
pub struct H1Client {}
12+
13+
impl Default for H1Client {
14+
fn default() -> Self {
15+
Self::new()
16+
}
17+
}
18+
19+
impl H1Client {
20+
/// Create a new instance.
21+
pub fn new() -> Self {
22+
Self {}
23+
}
24+
}
25+
26+
impl Clone for H1Client {
27+
fn clone(&self) -> Self {
28+
Self {}
29+
}
30+
}
31+
32+
impl HttpClient for H1Client {
33+
type Error = Error;
34+
35+
fn send(&self, req: Request) -> BoxFuture<'static, Result<Response, Self::Error>> {
36+
Box::pin(async move {
37+
// Insert host
38+
let host = req
39+
.url()
40+
.host_str()
41+
.ok_or_else(|| Error::from_str(StatusCode::BadRequest, "missing hostname"))?;
42+
43+
let scheme = req.url().scheme();
44+
if scheme != "http" && scheme != "https" {
45+
return Err(Error::from_str(
46+
StatusCode::BadRequest,
47+
format!("invalid url scheme '{}'", scheme),
48+
));
49+
}
50+
51+
let addr = req
52+
.url()
53+
.socket_addrs(|| match req.url().scheme() {
54+
"http" => Some(80),
55+
"https" => Some(443),
56+
_ => None,
57+
})?
58+
.into_iter()
59+
.next()
60+
.ok_or_else(|| Error::from_str(StatusCode::BadRequest, "missing valid address"))?;
61+
62+
log::trace!("> Scheme: {}", scheme);
63+
64+
match scheme {
65+
"http" => {
66+
let stream = async_std::net::TcpStream::connect(addr).await?;
67+
client::connect(stream, req).await
68+
}
69+
"https" => {
70+
let raw_stream = async_std::net::TcpStream::connect(addr).await?;
71+
72+
let stream = async_native_tls::connect(host, raw_stream).await?;
73+
74+
client::connect(stream, req).await
75+
}
76+
_ => unreachable!(),
77+
}
78+
})
79+
}
80+
}

src/isahc.rs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
33
use super::{Body, HttpClient, Request, Response};
44

5+
use async_std::io::BufReader;
56
use futures::future::BoxFuture;
7+
use isahc::http;
68

79
use std::sync::Arc;
810

@@ -46,25 +48,22 @@ impl HttpClient for IsahcClient {
4648
fn send(&self, req: Request) -> BoxFuture<'static, Result<Response, Self::Error>> {
4749
let client = self.client.clone();
4850
Box::pin(async move {
49-
let (parts, body) = req.into_parts();
50-
51-
let body = if body.is_empty() {
52-
isahc::Body::empty()
53-
} else {
54-
match body.len {
55-
Some(len) => isahc::Body::reader_sized(body, len),
56-
None => isahc::Body::reader(body),
57-
}
51+
let req_hyperium: http::Request<http_types::Body> = req.into();
52+
let (parts, body) = req_hyperium.into_parts();
53+
let body = match body.len() {
54+
Some(len) => isahc::Body::from_reader_sized(body, len as u64),
55+
None => isahc::Body::from_reader(body),
5856
};
59-
6057
let req: http::Request<isahc::Body> = http::Request::from_parts(parts, body);
6158

6259
let res = client.send_async(req).await?;
6360

6461
let (parts, body) = res.into_parts();
65-
let body = Body::from_reader(body);
62+
63+
let len = body.len().map(|len| len as usize);
64+
let body = Body::from_reader(BufReader::new(body), len);
6665
let res = http::Response::from_parts(parts, body);
67-
Ok(res)
66+
Ok(res.into())
6867
})
6968
}
7069
}

src/lib.rs

Lines changed: 11 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,6 @@
1515
)]
1616

1717
use futures::future::BoxFuture;
18-
use futures::io::{AsyncRead, Cursor};
19-
20-
use std::error::Error;
21-
use std::fmt::{self, Debug};
22-
use std::io;
23-
use std::pin::Pin;
24-
use std::task::{Context, Poll};
2518

2619
#[cfg_attr(feature = "docs", doc(cfg(curl_client)))]
2720
#[cfg(all(feature = "curl_client", not(target_arch = "wasm32")))]
@@ -35,11 +28,15 @@ pub mod wasm;
3528
#[cfg(feature = "native_client")]
3629
pub mod native;
3730

31+
#[cfg_attr(feature = "docs", doc(cfg(h1_client)))]
32+
#[cfg(feature = "h1_client")]
33+
pub mod h1;
34+
3835
/// An HTTP Request type with a streaming body.
39-
pub type Request = http::Request<Body>;
36+
pub type Request = http_types::Request;
4037

4138
/// An HTTP Response type with a streaming body.
42-
pub type Response = http::Response<Body>;
39+
pub type Response = http_types::Response;
4340

4441
/// An abstract HTTP client.
4542
///
@@ -55,90 +52,16 @@ pub type Response = http::Response<Body>;
5552
///
5653
/// How `Clone` is implemented is up to the implementors, but in an ideal scenario combining this
5754
/// with the `Client` builder will allow for high connection reuse, improving latency.
58-
pub trait HttpClient: Debug + Unpin + Send + Sync + Clone + 'static {
55+
pub trait HttpClient: std::fmt::Debug + Unpin + Send + Sync + Clone + 'static {
5956
/// The associated error type.
60-
type Error: Error + Send + Sync;
57+
type Error: Send + Sync + Into<Error>;
6158

6259
/// Perform a request.
6360
fn send(&self, req: Request) -> BoxFuture<'static, Result<Response, Self::Error>>;
6461
}
6562

6663
/// The raw body of an http request or response.
67-
///
68-
/// A body is a stream of `Bytes` values, which are shared handles to byte buffers.
69-
/// Both `Body` and `Bytes` values can be easily created from standard owned byte buffer types
70-
/// like `Vec<u8>` or `String`, using the `From` trait.
71-
pub struct Body {
72-
reader: Option<Box<dyn AsyncRead + Unpin + Send + 'static>>,
73-
/// Intentionally use `u64` over `usize` here.
74-
/// `usize` won't work if you try to send 10GB file from 32bit host.
75-
#[allow(dead_code)] // not all backends make use of this
76-
len: Option<u64>,
77-
}
78-
79-
impl Body {
80-
/// Create a new empty body.
81-
pub fn empty() -> Self {
82-
Self {
83-
reader: None,
84-
len: Some(0),
85-
}
86-
}
87-
88-
/// Create a new instance from a reader.
89-
pub fn from_reader(reader: impl AsyncRead + Unpin + Send + 'static) -> Self {
90-
Self {
91-
reader: Some(Box::new(reader)),
92-
len: None,
93-
}
94-
}
95-
96-
/// Validate that the body was created with `Body::empty()`.
97-
pub fn is_empty(&self) -> bool {
98-
self.reader.is_none()
99-
}
100-
}
64+
pub type Body = http_types::Body;
10165

102-
impl AsyncRead for Body {
103-
#[allow(missing_doc_code_examples)]
104-
fn poll_read(
105-
mut self: Pin<&mut Self>,
106-
cx: &mut Context<'_>,
107-
buf: &mut [u8],
108-
) -> Poll<io::Result<usize>> {
109-
match self.reader.as_mut() {
110-
Some(reader) => Pin::new(reader).poll_read(cx, buf),
111-
None => Poll::Ready(Ok(0)),
112-
}
113-
}
114-
}
115-
116-
impl fmt::Debug for Body {
117-
#[allow(missing_doc_code_examples)]
118-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119-
f.debug_struct("Body").field("reader", &"<hidden>").finish()
120-
}
121-
}
122-
123-
impl From<Vec<u8>> for Body {
124-
#[allow(missing_doc_code_examples)]
125-
#[inline]
126-
fn from(vec: Vec<u8>) -> Body {
127-
let len = vec.len() as u64;
128-
Self {
129-
reader: Some(Box::new(Cursor::new(vec))),
130-
len: Some(len),
131-
}
132-
}
133-
}
134-
135-
impl<R: AsyncRead + Unpin + Send + 'static> From<Box<R>> for Body {
136-
/// Converts an `AsyncRead` into a Body.
137-
#[allow(missing_doc_code_examples)]
138-
fn from(reader: Box<R>) -> Self {
139-
Self {
140-
reader: Some(reader),
141-
len: None,
142-
}
143-
}
144-
}
66+
/// Error type.
67+
pub type Error = http_types::Error;

0 commit comments

Comments
 (0)