Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 83 additions & 7 deletions src/http/client.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
use super::{response::IncomingBody, Body, Error, Request, Response, Result};
use crate::io::{self, AsyncWrite};

use wasi::http::types::OutgoingBody;

use super::{response::IncomingBody, Body, Request, Response, Result};
use crate::runtime::Reactor;
use std::time::Duration;
use wasi::clocks::monotonic_clock::Duration as WasiDuration;
use wasi::http::types::{OutgoingBody, RequestOptions as WasiRequestOptions};

/// An HTTP client.
// Empty for now, but permits adding support for RequestOptions soon:
#[derive(Debug)]
pub struct Client {}
pub struct Client {
options: Option<RequestOptions>,
}

impl Client {
/// Create a new instance of `Client`
pub fn new() -> Self {
Self {}
Self { options: None }
}

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

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

// 2. Start sending the request body
io::copy(body, OutputStream::new(body_stream))
Expand All @@ -42,6 +44,35 @@ impl Client {
let res = res.get().unwrap().unwrap()?;
Ok(Response::try_from_incoming_response(res)?)
}

/// Set timeout on connecting to HTTP server
pub fn set_connect_timeout(&mut self, d: Duration) {
self.options_mut().connect_timeout = Some(d);
}

/// Set timeout on recieving first byte of the Response body
pub fn set_first_byte_timeout(&mut self, d: Duration) {
self.options_mut().first_byte_timeout = Some(d);
}

/// Set timeout on recieving subsequent chunks of bytes in the Response body stream
pub fn set_between_bytes_timeout(&mut self, d: Duration) {
self.options_mut().between_bytes_timeout = Some(d);
}

fn options_mut(&mut self) -> &mut RequestOptions {
match &mut self.options {
Some(o) => o,
uninit => {
*uninit = Some(Default::default());
uninit.as_mut().unwrap()
}
}
}

fn wasi_options(&self) -> Result<Option<WasiRequestOptions>> {
self.options.as_ref().map(|o| o.to_wasi()).transpose()
}
}

struct OutputStream {
Expand Down Expand Up @@ -70,3 +101,48 @@ impl AsyncWrite for OutputStream {
Ok(())
}
}

#[derive(Default, Debug)]
struct RequestOptions {
connect_timeout: Option<Duration>,
first_byte_timeout: Option<Duration>,
between_bytes_timeout: Option<Duration>,
}

impl RequestOptions {
fn to_wasi(&self) -> Result<WasiRequestOptions> {
let wasi = WasiRequestOptions::new();
if let Some(timeout) = self.connect_timeout {
wasi.set_connect_timeout(Some(
dur_to_wasi(timeout).map_err(|e| e.context("connect timeout"))?,
))
.map_err(|()| {
Error::other("wasi-http implementation does not support connect timeout option")
})?;
}
if let Some(timeout) = self.first_byte_timeout {
wasi.set_first_byte_timeout(Some(
dur_to_wasi(timeout).map_err(|e| e.context("first byte timeout"))?,
))
.map_err(|()| {
Error::other("wasi-http implementation does not support first byte timeout option")
})?;
}
if let Some(timeout) = self.between_bytes_timeout {
wasi.set_between_bytes_timeout(Some(
dur_to_wasi(timeout).map_err(|e| e.context("between byte timeout"))?,
))
.map_err(|()| {
Error::other(
"wasi-http implementation does not support between byte timeout option",
)
})?;
}
Ok(wasi)
}
}
fn dur_to_wasi(d: Duration) -> Result<WasiDuration> {
d.as_nanos()
.try_into()
.map_err(|_| Error::other("duration out of range supported by wasi"))
}
37 changes: 22 additions & 15 deletions src/http/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ pub struct Error {
context: Vec<String>,
}

pub use http::header::{InvalidHeaderName, InvalidHeaderValue};
pub use http::method::InvalidMethod;
pub use wasi::http::types::{ErrorCode as WasiHttpErrorCode, HeaderError as WasiHttpHeaderError};

impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for c in self.context.iter() {
Expand Down Expand Up @@ -41,6 +45,9 @@ impl fmt::Display for Error {
impl std::error::Error for Error {}

impl Error {
pub fn variant(&self) -> &ErrorVariant {
&self.variant
}
pub(crate) fn other(s: impl Into<String>) -> Self {
ErrorVariant::Other(s.into()).into()
}
Expand All @@ -63,42 +70,42 @@ impl From<ErrorVariant> for Error {
}
}

impl From<wasi::http::types::ErrorCode> for Error {
fn from(e: wasi::http::types::ErrorCode) -> Error {
impl From<WasiHttpErrorCode> for Error {
fn from(e: WasiHttpErrorCode) -> Error {
ErrorVariant::WasiHttp(e).into()
}
}

impl From<wasi::http::types::HeaderError> for Error {
fn from(e: wasi::http::types::HeaderError) -> Error {
impl From<WasiHttpHeaderError> for Error {
fn from(e: WasiHttpHeaderError) -> Error {
ErrorVariant::WasiHeader(e).into()
}
}

impl From<http::header::InvalidHeaderValue> for Error {
fn from(e: http::header::InvalidHeaderValue) -> Error {
impl From<InvalidHeaderValue> for Error {
fn from(e: InvalidHeaderValue) -> Error {
ErrorVariant::HeaderValue(e).into()
}
}

impl From<http::header::InvalidHeaderName> for Error {
fn from(e: http::header::InvalidHeaderName) -> Error {
impl From<InvalidHeaderName> for Error {
fn from(e: InvalidHeaderName) -> Error {
ErrorVariant::HeaderName(e).into()
}
}

impl From<http::method::InvalidMethod> for Error {
fn from(e: http::method::InvalidMethod) -> Error {
impl From<InvalidMethod> for Error {
fn from(e: InvalidMethod) -> Error {
ErrorVariant::Method(e).into()
}
}

#[derive(Debug)]
pub enum ErrorVariant {
WasiHttp(wasi::http::types::ErrorCode),
WasiHeader(wasi::http::types::HeaderError),
HeaderName(http::header::InvalidHeaderName),
HeaderValue(http::header::InvalidHeaderValue),
Method(http::method::InvalidMethod),
WasiHttp(WasiHttpErrorCode),
WasiHeader(WasiHttpHeaderError),
HeaderName(InvalidHeaderName),
HeaderValue(InvalidHeaderValue),
Method(InvalidMethod),
Other(String),
}
2 changes: 1 addition & 1 deletion src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub use status_code::StatusCode;
pub mod body;

mod client;
mod error;
pub mod error;
mod fields;
mod method;
mod request;
Expand Down
27 changes: 27 additions & 0 deletions test-programs/src/bin/http_first_byte_timeout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use wstd::http::{
error::{ErrorVariant, WasiHttpErrorCode},
Client, Method, Request,
};

#[wstd::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Set first byte timeout to 1/2 second.
let mut client = Client::new();
client.set_first_byte_timeout(std::time::Duration::from_millis(500));
// This get request will connect to the server, which will then wait 1 second before
// returning a response.
let request = Request::new(Method::GET, "https://postman-echo.com/delay/1".parse()?);
let result = client.send(request).await;

assert!(result.is_err(), "response should be an error");
let error = result.unwrap_err();
assert!(
matches!(
error.variant(),
ErrorVariant::WasiHttp(WasiHttpErrorCode::ConnectionReadTimeout)
),
"expected ConnectionReadTimeout error, got: {error:?>}"
);

Ok(())
}
10 changes: 10 additions & 0 deletions tests/test-programs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ fn tcp_echo_server() -> Result<()> {
fn http_get() -> Result<()> {
println!("testing {}", test_programs_artifacts::HTTP_GET);
let wasm = std::fs::read(test_programs_artifacts::HTTP_GET).context("read wasm")?;
run_in_wasmtime(&wasm, None)
}

#[test]
fn http_first_byte_timeout() -> Result<()> {
println!(
"testing {}",
test_programs_artifacts::HTTP_FIRST_BYTE_TIMEOUT
);
let wasm =
std::fs::read(test_programs_artifacts::HTTP_FIRST_BYTE_TIMEOUT).context("read wasm")?;
run_in_wasmtime(&wasm, None)
}