Skip to content

Commit 84519fd

Browse files
authored
Merge pull request #20 from yoshuawuyts/pch/request_options
http client: support request options
2 parents a09ebd4 + ad33e9f commit 84519fd

File tree

5 files changed

+143
-23
lines changed

5 files changed

+143
-23
lines changed

src/http/client.rs

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1+
use super::{response::IncomingBody, Body, Error, Request, Response, Result};
12
use crate::io::{self, AsyncWrite};
2-
3-
use wasi::http::types::OutgoingBody;
4-
5-
use super::{response::IncomingBody, Body, Request, Response, Result};
63
use crate::runtime::Reactor;
4+
use std::time::Duration;
5+
use wasi::clocks::monotonic_clock::Duration as WasiDuration;
6+
use wasi::http::types::{OutgoingBody, RequestOptions as WasiRequestOptions};
77

88
/// An HTTP client.
99
// Empty for now, but permits adding support for RequestOptions soon:
1010
#[derive(Debug)]
11-
pub struct Client {}
11+
pub struct Client {
12+
options: Option<RequestOptions>,
13+
}
1214

1315
impl Client {
1416
/// Create a new instance of `Client`
1517
pub fn new() -> Self {
16-
Self {}
18+
Self { options: None }
1719
}
1820

1921
/// Send an HTTP request.
@@ -23,7 +25,7 @@ impl Client {
2325
let body_stream = wasi_body.write().unwrap();
2426

2527
// 1. Start sending the request head
26-
let res = wasi::http::outgoing_handler::handle(wasi_req, None).unwrap();
28+
let res = wasi::http::outgoing_handler::handle(wasi_req, self.wasi_options()?).unwrap();
2729

2830
// 2. Start sending the request body
2931
io::copy(body, OutputStream::new(body_stream))
@@ -42,6 +44,35 @@ impl Client {
4244
let res = res.get().unwrap().unwrap()?;
4345
Ok(Response::try_from_incoming_response(res)?)
4446
}
47+
48+
/// Set timeout on connecting to HTTP server
49+
pub fn set_connect_timeout(&mut self, d: Duration) {
50+
self.options_mut().connect_timeout = Some(d);
51+
}
52+
53+
/// Set timeout on recieving first byte of the Response body
54+
pub fn set_first_byte_timeout(&mut self, d: Duration) {
55+
self.options_mut().first_byte_timeout = Some(d);
56+
}
57+
58+
/// Set timeout on recieving subsequent chunks of bytes in the Response body stream
59+
pub fn set_between_bytes_timeout(&mut self, d: Duration) {
60+
self.options_mut().between_bytes_timeout = Some(d);
61+
}
62+
63+
fn options_mut(&mut self) -> &mut RequestOptions {
64+
match &mut self.options {
65+
Some(o) => o,
66+
uninit => {
67+
*uninit = Some(Default::default());
68+
uninit.as_mut().unwrap()
69+
}
70+
}
71+
}
72+
73+
fn wasi_options(&self) -> Result<Option<WasiRequestOptions>> {
74+
self.options.as_ref().map(|o| o.to_wasi()).transpose()
75+
}
4576
}
4677

4778
struct OutputStream {
@@ -70,3 +101,48 @@ impl AsyncWrite for OutputStream {
70101
Ok(())
71102
}
72103
}
104+
105+
#[derive(Default, Debug)]
106+
struct RequestOptions {
107+
connect_timeout: Option<Duration>,
108+
first_byte_timeout: Option<Duration>,
109+
between_bytes_timeout: Option<Duration>,
110+
}
111+
112+
impl RequestOptions {
113+
fn to_wasi(&self) -> Result<WasiRequestOptions> {
114+
let wasi = WasiRequestOptions::new();
115+
if let Some(timeout) = self.connect_timeout {
116+
wasi.set_connect_timeout(Some(
117+
dur_to_wasi(timeout).map_err(|e| e.context("connect timeout"))?,
118+
))
119+
.map_err(|()| {
120+
Error::other("wasi-http implementation does not support connect timeout option")
121+
})?;
122+
}
123+
if let Some(timeout) = self.first_byte_timeout {
124+
wasi.set_first_byte_timeout(Some(
125+
dur_to_wasi(timeout).map_err(|e| e.context("first byte timeout"))?,
126+
))
127+
.map_err(|()| {
128+
Error::other("wasi-http implementation does not support first byte timeout option")
129+
})?;
130+
}
131+
if let Some(timeout) = self.between_bytes_timeout {
132+
wasi.set_between_bytes_timeout(Some(
133+
dur_to_wasi(timeout).map_err(|e| e.context("between byte timeout"))?,
134+
))
135+
.map_err(|()| {
136+
Error::other(
137+
"wasi-http implementation does not support between byte timeout option",
138+
)
139+
})?;
140+
}
141+
Ok(wasi)
142+
}
143+
}
144+
fn dur_to_wasi(d: Duration) -> Result<WasiDuration> {
145+
d.as_nanos()
146+
.try_into()
147+
.map_err(|_| Error::other("duration out of range supported by wasi"))
148+
}

src/http/error.rs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ pub struct Error {
99
context: Vec<String>,
1010
}
1111

12+
pub use http::header::{InvalidHeaderName, InvalidHeaderValue};
13+
pub use http::method::InvalidMethod;
14+
pub use wasi::http::types::{ErrorCode as WasiHttpErrorCode, HeaderError as WasiHttpHeaderError};
15+
1216
impl fmt::Debug for Error {
1317
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1418
for c in self.context.iter() {
@@ -41,6 +45,9 @@ impl fmt::Display for Error {
4145
impl std::error::Error for Error {}
4246

4347
impl Error {
48+
pub fn variant(&self) -> &ErrorVariant {
49+
&self.variant
50+
}
4451
pub(crate) fn other(s: impl Into<String>) -> Self {
4552
ErrorVariant::Other(s.into()).into()
4653
}
@@ -63,42 +70,42 @@ impl From<ErrorVariant> for Error {
6370
}
6471
}
6572

66-
impl From<wasi::http::types::ErrorCode> for Error {
67-
fn from(e: wasi::http::types::ErrorCode) -> Error {
73+
impl From<WasiHttpErrorCode> for Error {
74+
fn from(e: WasiHttpErrorCode) -> Error {
6875
ErrorVariant::WasiHttp(e).into()
6976
}
7077
}
7178

72-
impl From<wasi::http::types::HeaderError> for Error {
73-
fn from(e: wasi::http::types::HeaderError) -> Error {
79+
impl From<WasiHttpHeaderError> for Error {
80+
fn from(e: WasiHttpHeaderError) -> Error {
7481
ErrorVariant::WasiHeader(e).into()
7582
}
7683
}
7784

78-
impl From<http::header::InvalidHeaderValue> for Error {
79-
fn from(e: http::header::InvalidHeaderValue) -> Error {
85+
impl From<InvalidHeaderValue> for Error {
86+
fn from(e: InvalidHeaderValue) -> Error {
8087
ErrorVariant::HeaderValue(e).into()
8188
}
8289
}
8390

84-
impl From<http::header::InvalidHeaderName> for Error {
85-
fn from(e: http::header::InvalidHeaderName) -> Error {
91+
impl From<InvalidHeaderName> for Error {
92+
fn from(e: InvalidHeaderName) -> Error {
8693
ErrorVariant::HeaderName(e).into()
8794
}
8895
}
8996

90-
impl From<http::method::InvalidMethod> for Error {
91-
fn from(e: http::method::InvalidMethod) -> Error {
97+
impl From<InvalidMethod> for Error {
98+
fn from(e: InvalidMethod) -> Error {
9299
ErrorVariant::Method(e).into()
93100
}
94101
}
95102

96103
#[derive(Debug)]
97104
pub enum ErrorVariant {
98-
WasiHttp(wasi::http::types::ErrorCode),
99-
WasiHeader(wasi::http::types::HeaderError),
100-
HeaderName(http::header::InvalidHeaderName),
101-
HeaderValue(http::header::InvalidHeaderValue),
102-
Method(http::method::InvalidMethod),
105+
WasiHttp(WasiHttpErrorCode),
106+
WasiHeader(WasiHttpHeaderError),
107+
HeaderName(InvalidHeaderName),
108+
HeaderValue(InvalidHeaderValue),
109+
Method(InvalidMethod),
103110
Other(String),
104111
}

src/http/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub use status_code::StatusCode;
1515
pub mod body;
1616

1717
mod client;
18-
mod error;
18+
pub mod error;
1919
mod fields;
2020
mod method;
2121
mod request;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use wstd::http::{
2+
error::{ErrorVariant, WasiHttpErrorCode},
3+
Client, Method, Request,
4+
};
5+
6+
#[wstd::main]
7+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
8+
// Set first byte timeout to 1/2 second.
9+
let mut client = Client::new();
10+
client.set_first_byte_timeout(std::time::Duration::from_millis(500));
11+
// This get request will connect to the server, which will then wait 1 second before
12+
// returning a response.
13+
let request = Request::new(Method::GET, "https://postman-echo.com/delay/1".parse()?);
14+
let result = client.send(request).await;
15+
16+
assert!(result.is_err(), "response should be an error");
17+
let error = result.unwrap_err();
18+
assert!(
19+
matches!(
20+
error.variant(),
21+
ErrorVariant::WasiHttp(WasiHttpErrorCode::ConnectionReadTimeout)
22+
),
23+
"expected ConnectionReadTimeout error, got: {error:?>}"
24+
);
25+
26+
Ok(())
27+
}

tests/test-programs.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ fn tcp_echo_server() -> Result<()> {
124124
fn http_get() -> Result<()> {
125125
println!("testing {}", test_programs_artifacts::HTTP_GET);
126126
let wasm = std::fs::read(test_programs_artifacts::HTTP_GET).context("read wasm")?;
127+
run_in_wasmtime(&wasm, None)
128+
}
127129

130+
#[test]
131+
fn http_first_byte_timeout() -> Result<()> {
132+
println!(
133+
"testing {}",
134+
test_programs_artifacts::HTTP_FIRST_BYTE_TIMEOUT
135+
);
136+
let wasm =
137+
std::fs::read(test_programs_artifacts::HTTP_FIRST_BYTE_TIMEOUT).context("read wasm")?;
128138
run_in_wasmtime(&wasm, None)
129139
}

0 commit comments

Comments
 (0)