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
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! `between-bytes-timeout` bounds the wait for each subsequent chunk of the body.
//!
//! The server sends the response headers plus one chunk, then stalls well past the
//! timeout, so reading the second chunk has to fail rather than wait for it.
//! `http.wit` documents these timeouts as transport-layer timeouts, so the expected
//! failure is a read timeout — and `io.wit` warns that the debug string of an error
//! "should not be consumed mechanically", so this only asserts the error's kind.
use anyhow::{Result, anyhow};
use test_programs::wasi::http::{outgoing_handler, types as http_types};
use test_programs::wasi::io::streams;

fn main() -> Result<()> {
let addr = std::env::var("HTTP_SERVER").unwrap();

let headers =
http_types::Headers::from_list(&[("User-agent".to_string(), b"WASI-HTTP/0.0.1".to_vec())])?;
let request = http_types::OutgoingRequest::new(headers);
request
.set_method(&http_types::Method::Get)
.map_err(|()| anyhow!("set_method"))?;
request
.set_scheme(Some(&http_types::Scheme::Http))
.map_err(|()| anyhow!("set_scheme"))?;
request
.set_authority(Some(&addr))
.map_err(|()| anyhow!("set_authority"))?;
request
.set_path_with_query(Some("/between-bytes-timeout"))
.map_err(|()| anyhow!("set_path"))?;

let outgoing_body = request.body().map_err(|_| anyhow!("request.body"))?;

let options = http_types::RequestOptions::new();
options
.set_between_bytes_timeout(Some(500_000_000)) // 500ms
.map_err(|()| anyhow!("set_between_bytes_timeout"))?;

let future_response = outgoing_handler::handle(request, Some(options))?;
http_types::OutgoingBody::finish(outgoing_body, None)?;

let incoming_response = match future_response.get() {
Some(r) => r.map_err(|()| anyhow!("taken"))??,
None => {
future_response.subscribe().block();
future_response
.get()
.expect("available")
.map_err(|()| anyhow!("taken"))??
}
};

let incoming_body = incoming_response
.consume()
.map_err(|()| anyhow!("consume"))?;
let stream = incoming_body.stream().map_err(|()| anyhow!("stream"))?;
let pollable = stream.subscribe();

// The first chunk has already been sent by the server, so it must arrive.
pollable.block();
let first = stream
.read(1024)
.map_err(|e| anyhow!("reading the first chunk failed: {e:?}"))?;
assert!(
!first.is_empty(),
"the first chunk should have arrived before the stall"
);

// The second chunk is stalled well past the timeout, so this read must fail.
pollable.block();
let err = stream
.read(1024)
.expect_err("reading the stalled second chunk should have timed out");
assert!(
matches!(err, streams::StreamError::LastOperationFailed(_)),
"expected the read to fail with a last-operation failure: {err:?}"
);

Ok(())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//! `first-byte-timeout` bounds the wait for the first byte of the response.
//!
//! The server accepts the connection but delays its response headers well past
//! the timeout, so the request has to fail rather than wait for the headers.
//! `http.wit` documents these timeouts as transport-layer timeouts, separate
//! from any the user may use to bound a request, so the expected error is a
//! read timeout.
use std::time::Duration;

use test_programs::wasi::http::types::{ErrorCode, Method, Scheme};

fn main() {
let addr = std::env::var("HTTP_SERVER").unwrap();
let first_byte = Duration::from_millis(500).as_nanos() as u64;

let res = test_programs::http::request(
Method::Get,
Scheme::Http,
&addr,
"/first-byte-timeout",
None,
None,
None, // connect_timeout
Some(first_byte), // first_byte_timeout
None, // between_bytes_timeout
);

let err = match res {
Ok(_) => panic!("the delayed response headers should have timed out"),
Err(err) => err,
};
assert!(
matches!(
err.downcast_ref::<ErrorCode>(),
Some(ErrorCode::ConnectionReadTimeout)
),
"expected a connection read timeout: {err:?}"
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//! `first-byte-timeout` bounds the wait for the first byte of the response.
//!
//! The server accepts the connection but delays its response headers well past
//! the timeout, so the request has to fail rather than wait for the headers.
//! `http.wit` documents these timeouts as transport-layer timeouts, separate
//! from any the user may use to bound a request, so the expected error is a
//! read timeout.
use std::time::Duration;

use test_programs::p3::wasi::http::types::{ErrorCode, Method, Scheme};

struct Component;

test_programs::p3::export!(Component);

impl test_programs::p3::exports::wasi::cli::run::Guest for Component {
async fn run() -> Result<(), ()> {
let addr = test_programs::p3::wasi::cli::environment::get_environment()
.into_iter()
.find_map(|(k, v)| k.eq("HTTP_SERVER").then_some(v))
.unwrap();
let first_byte = Duration::from_millis(500).as_nanos() as u64;

let res = test_programs::p3::http::request(
Method::Get,
Scheme::Http,
&addr,
"/first-byte-timeout",
None,
None,
None, // connect_timeout
Some(first_byte), // first_byte_timeout
None, // between_bytes_timeout
)
.await;

let err = match res {
Ok(_) => panic!("the delayed response headers should have timed out"),
Err(err) => err,
};
assert!(
matches!(
err.downcast_ref::<ErrorCode>(),
Some(ErrorCode::ConnectionReadTimeout)
),
"expected a connection read timeout: {err:?}"
);
Ok(())
}
}

fn main() {}
48 changes: 48 additions & 0 deletions crates/wasi-http/tests/all/http_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,54 @@ impl Server {
})
}

/// Test-only: delay the response headers, to exercise `first-byte-timeout`.
pub fn http1_delayed(conns: usize, header_delay: std::time::Duration) -> Result<Self> {
debug!("initializing delayed http1 server");
Self::new(conns, move |io| async move {
let service = service_fn(move |_req| async move {
tokio::time::sleep(header_delay).await;
Ok::<_, std::convert::Infallible>(Response::new(crate::body::full(
hyper::body::Bytes::new(),
)))
});
let mut builder = hyper::server::conn::http1::Builder::new();
let http = builder.keep_alive(false).pipeline_flush(true);
let conn = http.serve_connection(io, service).await;
conn?;
Ok(())
})
}

/// Test-only: send the response headers plus one chunk of body, then stall.
/// Exercises `between-bytes-timeout`.
pub fn http1_stalled_body(conns: usize, stall: std::time::Duration) -> Result<Self> {
debug!("initializing stalled-body http1 server");
Self::new(conns, move |io| async move {
let service = service_fn(move |_req| async move {
use futures::StreamExt as _;
let stream = futures::stream::once(async {
Ok::<_, std::io::Error>(hyper::body::Frame::data(
hyper::body::Bytes::from_static(b"first"),
))
})
.chain(futures::stream::once(async move {
tokio::time::sleep(stall).await;
Ok(hyper::body::Frame::data(hyper::body::Bytes::from_static(
b"second",
)))
}));
Ok::<_, std::convert::Infallible>(Response::new(http_body_util::BodyExt::boxed(
http_body_util::StreamBody::new(stream),
)))
});
let mut builder = hyper::server::conn::http1::Builder::new();
let http = builder.keep_alive(false).pipeline_flush(true);
let conn = http.serve_connection(io, service).await;
conn?;
Ok(())
})
}

pub fn http2(conns: usize) -> Result<Self> {
debug!("initializing http2 server");
Self::new(conns, |io| async move {
Expand Down
20 changes: 20 additions & 0 deletions crates/wasi-http/tests/all/p2/async_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@ async fn p2_http_outbound_request_get() -> Result<()> {
run(P2_HTTP_OUTBOUND_REQUEST_GET_COMPONENT, &server).await
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn p2_http_outbound_request_between_bytes_timeout() -> Result<()> {
let server = Server::http1_stalled_body(1, std::time::Duration::from_secs(5))?;
run(
P2_HTTP_OUTBOUND_REQUEST_BETWEEN_BYTES_TIMEOUT_COMPONENT,
&server,
)
.await
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn p2_http_outbound_request_first_byte_timeout() -> Result<()> {
let server = Server::http1_delayed(1, std::time::Duration::from_secs(5))?;
run(
P2_HTTP_OUTBOUND_REQUEST_FIRST_BYTE_TIMEOUT_COMPONENT,
&server,
)
.await
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn p2_http_outbound_request_timeout() -> Result<()> {
let server = Server::http1(3)?;
Expand Down
18 changes: 18 additions & 0 deletions crates/wasi-http/tests/all/p2/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ fn p2_http_outbound_request_get() -> Result<()> {
run(P2_HTTP_OUTBOUND_REQUEST_GET_COMPONENT, &server)
}

#[test_log::test]
fn p2_http_outbound_request_between_bytes_timeout() -> Result<()> {
let server = Server::http1_stalled_body(1, std::time::Duration::from_secs(5))?;
run(
P2_HTTP_OUTBOUND_REQUEST_BETWEEN_BYTES_TIMEOUT_COMPONENT,
&server,
)
}

#[test_log::test]
fn p2_http_outbound_request_first_byte_timeout() -> Result<()> {
let server = Server::http1_delayed(1, std::time::Duration::from_secs(5))?;
run(
P2_HTTP_OUTBOUND_REQUEST_FIRST_BYTE_TIMEOUT_COMPONENT,
&server,
)
}

#[test_log::test]
fn p2_http_outbound_request_timeout() -> Result<()> {
let server = Server::http1(3)?;
Expand Down
10 changes: 10 additions & 0 deletions crates/wasi-http/tests/all/p3/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,16 @@ async fn p3_http_outbound_request_get() -> wasmtime::Result<()> {
run_cli(P3_HTTP_OUTBOUND_REQUEST_GET_COMPONENT, &server).await
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn p3_http_outbound_request_first_byte_timeout() -> wasmtime::Result<()> {
let server = Server::http1_delayed(1, std::time::Duration::from_secs(5))?;
run_cli(
P3_HTTP_OUTBOUND_REQUEST_FIRST_BYTE_TIMEOUT_COMPONENT,
&server,
)
.await
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn p3_http_outbound_request_timeout() -> wasmtime::Result<()> {
let server = Server::http1(3)?;
Expand Down
Loading