Skip to content

Commit da8d299

Browse files
authored
Merge pull request #34 from sunfishcode/sunfishcode/proxy
HTTP streaming client, and trailer support
2 parents cf36c89 + d8a10e4 commit da8d299

File tree

5 files changed

+391
-5
lines changed

5 files changed

+391
-5
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ wstd-macro.workspace = true
2323

2424
[dev-dependencies]
2525
anyhow.workspace = true
26+
clap.workspace = true
2627
futures-lite.workspace = true
28+
humantime.workspace = true
2729
serde_json.workspace = true
2830

2931
[workspace]
@@ -44,13 +46,16 @@ categories = []
4446
authors = [
4547
"Yoshua Wuyts <[email protected]>",
4648
"Pat Hickey <[email protected]>",
49+
"Dan Gohman <[email protected]>",
4750
]
4851

4952
[workspace.dependencies]
5053
anyhow = "1"
5154
cargo_metadata = "0.18.1"
55+
clap = { version = "4.5.26", features = ["derive"] }
5256
futures-core = "0.3.19"
5357
futures-lite = "1.12.0"
58+
humantime = "2.1.0"
5459
heck = "0.5"
5560
http = "1.1"
5661
itoa = "1"

examples/complex_http_client.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use anyhow::{anyhow, Result};
2+
use clap::{ArgAction, Parser};
3+
use std::str::FromStr;
4+
use wstd::http::{
5+
body::BodyForthcoming, Client, HeaderMap, HeaderName, HeaderValue, Method, Request, Uri,
6+
};
7+
8+
/// Complex HTTP client
9+
///
10+
/// A somewhat more complex command-line HTTP client, implemented using
11+
/// `wstd`, using WASI.
12+
#[derive(Parser, Debug)]
13+
#[command(version, about)]
14+
struct Args {
15+
/// The URL to request
16+
url: Uri,
17+
18+
/// Forward stdin to the request body
19+
#[arg(long)]
20+
body: bool,
21+
22+
/// Add a header to the request
23+
#[arg(long = "header", action = ArgAction::Append, value_name = "HEADER")]
24+
headers: Vec<String>,
25+
26+
/// Add a trailer to the request
27+
#[arg(long = "trailer", action = ArgAction::Append, value_name = "TRAILER")]
28+
trailers: Vec<String>,
29+
30+
/// Method of the request
31+
#[arg(long, default_value = "GET")]
32+
method: Method,
33+
34+
/// Set the connect timeout
35+
#[arg(long, value_name = "DURATION")]
36+
connect_timeout: Option<humantime::Duration>,
37+
38+
/// Set the first-byte timeout
39+
#[arg(long, value_name = "DURATION")]
40+
first_byte_timeout: Option<humantime::Duration>,
41+
42+
/// Set the between-bytes timeout
43+
#[arg(long, value_name = "DURATION")]
44+
between_bytes_timeout: Option<humantime::Duration>,
45+
}
46+
47+
#[wstd::main]
48+
async fn main() -> Result<()> {
49+
let args = Args::parse();
50+
51+
// Create and configure the `Client`
52+
53+
let mut client = Client::new();
54+
55+
if let Some(connect_timeout) = args.connect_timeout {
56+
client.set_connect_timeout(*connect_timeout);
57+
}
58+
if let Some(first_byte_timeout) = args.first_byte_timeout {
59+
client.set_first_byte_timeout(*first_byte_timeout);
60+
}
61+
if let Some(between_bytes_timeout) = args.between_bytes_timeout {
62+
client.set_between_bytes_timeout(*between_bytes_timeout);
63+
}
64+
65+
// Create and configure the request.
66+
67+
let mut request = Request::builder();
68+
69+
request = request.uri(args.url).method(args.method);
70+
71+
for header in args.headers {
72+
let mut parts = header.splitn(2, ": ");
73+
let key = parts.next().unwrap();
74+
let value = parts
75+
.next()
76+
.ok_or_else(|| anyhow!("headers must be formatted like \"key: value\""))?;
77+
request = request.header(key, value);
78+
}
79+
let mut trailers = HeaderMap::new();
80+
for trailer in args.trailers {
81+
let mut parts = trailer.splitn(2, ": ");
82+
let key = parts.next().unwrap();
83+
let value = parts
84+
.next()
85+
.ok_or_else(|| anyhow!("trailers must be formatted like \"key: value\""))?;
86+
trailers.insert(HeaderName::from_str(key)?, HeaderValue::from_str(value)?);
87+
}
88+
89+
// Send the request.
90+
91+
let request = request.body(BodyForthcoming)?;
92+
93+
eprintln!("> {} / {:?}", request.method(), request.version());
94+
for (key, value) in request.headers().iter() {
95+
let value = String::from_utf8_lossy(value.as_bytes());
96+
eprintln!("> {key}: {value}");
97+
}
98+
99+
let (mut outgoing_body, response) = client.start_request(request).await?;
100+
101+
if args.body {
102+
wstd::io::copy(wstd::io::stdin(), &mut outgoing_body).await?;
103+
} else {
104+
wstd::io::copy(wstd::io::empty(), &mut outgoing_body).await?;
105+
}
106+
107+
if !trailers.is_empty() {
108+
eprintln!("...");
109+
}
110+
for (key, value) in trailers.iter() {
111+
let value = String::from_utf8_lossy(value.as_bytes());
112+
eprintln!("> {key}: {value}");
113+
}
114+
115+
Client::finish(outgoing_body, Some(trailers))?;
116+
117+
let response = response.await?;
118+
119+
// Print the response.
120+
121+
eprintln!("< {:?} {}", response.version(), response.status());
122+
for (key, value) in response.headers().iter() {
123+
let value = String::from_utf8_lossy(value.as_bytes());
124+
eprintln!("< {key}: {value}");
125+
}
126+
127+
let mut body = response.into_body();
128+
wstd::io::copy(&mut body, wstd::io::stdout()).await?;
129+
130+
let trailers = body.finish().await?;
131+
if let Some(trailers) = trailers {
132+
for (key, value) in trailers.iter() {
133+
let value = String::from_utf8_lossy(value.as_bytes());
134+
eprintln!("< {key}: {value}");
135+
}
136+
}
137+
138+
Ok(())
139+
}

examples/http_client.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
use anyhow::{anyhow, Result};
2+
use clap::{ArgAction, Parser};
3+
use wstd::http::{
4+
body::{IncomingBody, StreamedBody},
5+
request::Builder,
6+
Body, Client, Method, Request, Response, Uri,
7+
};
8+
9+
/// Simple HTTP client
10+
///
11+
/// A simple command-line HTTP client, implemented using `wstd`, using WASI.
12+
#[derive(Parser, Debug)]
13+
#[command(version, about)]
14+
struct Args {
15+
/// The URL to request
16+
url: Uri,
17+
18+
/// Forward stdin to the request body
19+
#[arg(long)]
20+
body: bool,
21+
22+
/// Add a header to the request
23+
#[arg(long = "header", action = ArgAction::Append, value_name = "HEADER")]
24+
headers: Vec<String>,
25+
26+
/// Method of the request
27+
#[arg(long, default_value = "GET")]
28+
method: Method,
29+
30+
/// Set the connect timeout
31+
#[arg(long, value_name = "DURATION")]
32+
connect_timeout: Option<humantime::Duration>,
33+
34+
/// Set the first-byte timeout
35+
#[arg(long, value_name = "DURATION")]
36+
first_byte_timeout: Option<humantime::Duration>,
37+
38+
/// Set the between-bytes timeout
39+
#[arg(long, value_name = "DURATION")]
40+
between_bytes_timeout: Option<humantime::Duration>,
41+
}
42+
43+
#[wstd::main]
44+
async fn main() -> Result<()> {
45+
let args = Args::parse();
46+
47+
// Create and configure the `Client`
48+
49+
let mut client = Client::new();
50+
51+
if let Some(connect_timeout) = args.connect_timeout {
52+
client.set_connect_timeout(*connect_timeout);
53+
}
54+
if let Some(first_byte_timeout) = args.first_byte_timeout {
55+
client.set_first_byte_timeout(*first_byte_timeout);
56+
}
57+
if let Some(between_bytes_timeout) = args.between_bytes_timeout {
58+
client.set_between_bytes_timeout(*between_bytes_timeout);
59+
}
60+
61+
// Create and configure the request.
62+
63+
let mut request = Request::builder();
64+
65+
request = request.uri(args.url).method(args.method);
66+
67+
for header in args.headers {
68+
let mut parts = header.splitn(2, ": ");
69+
let key = parts.next().unwrap();
70+
let value = parts
71+
.next()
72+
.ok_or_else(|| anyhow!("headers must be formatted like \"key: value\""))?;
73+
request = request.header(key, value);
74+
}
75+
76+
// Send the request.
77+
78+
async fn send_request<B: Body>(
79+
client: &Client,
80+
request: Builder,
81+
body: B,
82+
) -> Result<Response<IncomingBody>> {
83+
let request = request.body(body)?;
84+
85+
eprintln!("> {} / {:?}", request.method(), request.version());
86+
for (key, value) in request.headers().iter() {
87+
let value = String::from_utf8_lossy(value.as_bytes());
88+
eprintln!("> {key}: {value}");
89+
}
90+
91+
Ok(client.send(request).await?)
92+
}
93+
let response = if args.body {
94+
send_request(&client, request, StreamedBody::new(wstd::io::stdin())).await
95+
} else {
96+
send_request(&client, request, wstd::io::empty()).await
97+
}?;
98+
99+
// Print the response.
100+
101+
eprintln!("< {:?} {}", response.version(), response.status());
102+
for (key, value) in response.headers().iter() {
103+
let value = String::from_utf8_lossy(value.as_bytes());
104+
eprintln!("< {key}: {value}");
105+
}
106+
107+
let mut body = response.into_body();
108+
wstd::io::copy(&mut body, wstd::io::stdout()).await?;
109+
110+
let trailers = body.finish().await?;
111+
if let Some(trailers) = trailers {
112+
for (key, value) in trailers.iter() {
113+
let value = String::from_utf8_lossy(value.as_bytes());
114+
eprintln!("< {key}: {value}");
115+
}
116+
}
117+
118+
Ok(())
119+
}

src/http/body.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,27 @@ impl<T: AsRef<[u8]>> Body for BoundedBody<T> {
107107
}
108108
}
109109

110+
/// An HTTP body with an unknown length
111+
#[derive(Debug)]
112+
pub struct StreamedBody<S: AsyncRead>(S);
113+
114+
impl<S: AsyncRead> StreamedBody<S> {
115+
/// Wrap an `AsyncRead` impl in a type that provides a [`Body`] implementation.
116+
pub fn new(s: S) -> Self {
117+
Self(s)
118+
}
119+
}
120+
impl<S: AsyncRead> AsyncRead for StreamedBody<S> {
121+
async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result<usize> {
122+
self.0.read(buf).await
123+
}
124+
}
125+
impl<S: AsyncRead> Body for StreamedBody<S> {
126+
fn len(&self) -> Option<usize> {
127+
None
128+
}
129+
}
130+
110131
impl Body for Empty {
111132
fn len(&self) -> Option<usize> {
112133
Some(0)

0 commit comments

Comments
 (0)