diff --git a/examples/http_server_proxy.rs b/examples/http_server_proxy.rs
new file mode 100644
index 0000000..ccad608
--- /dev/null
+++ b/examples/http_server_proxy.rs
@@ -0,0 +1,64 @@
+//! Run the example with:
+//! ```sh
+//! cargo build --example http_server_proxy --target=wasm32-wasip2
+//! wasmtime serve -Scli -Shttp --env TARGET_URL=https://example.com/ target/wasm32-wasip2/debug/examples/http_server_proxy.wasm
+//! curl --no-buffer -v 127.0.0.1:8080/proxy/
+//! ```
+use wstd::http::body::Body;
+use wstd::http::{Client, Error, Request, Response, StatusCode, Uri};
+
+const PROXY_PREFIX: &str = "/proxy/";
+
+#[wstd::http_server]
+async fn main(server_req: Request
) -> Result, Error> {
+ match server_req.uri().path_and_query().unwrap().as_str() {
+ api_prefixed_path if api_prefixed_path.starts_with(PROXY_PREFIX) => {
+ // Remove PROXY_PREFIX
+ let target_url =
+ std::env::var("TARGET_URL").expect("missing environment variable TARGET_URL");
+ let target_url: Uri = format!(
+ "{target_url}{}",
+ api_prefixed_path
+ .strip_prefix(PROXY_PREFIX)
+ .expect("checked above")
+ )
+ .parse()
+ .expect("final target url should be parseable");
+ println!("Proxying to {target_url}");
+ proxy(server_req, target_url).await
+ }
+ _ => Ok(http_not_found(server_req)),
+ }
+}
+
+async fn proxy(server_req: Request, target_url: Uri) -> Result, Error> {
+ let client = Client::new();
+ let mut client_req = Request::builder();
+ client_req = client_req.uri(target_url).method(server_req.method());
+
+ // Copy headers from `server_req` to the `client_req`.
+ for (key, value) in server_req.headers() {
+ client_req = client_req.header(key, value);
+ }
+
+ // Stream the request body.
+ let client_req = client_req.body(server_req.into_body())?;
+ // Send the request.
+ let client_resp = client.send(client_req).await?;
+ // Copy headers from `client_resp` to `server_resp`.
+ let mut server_resp = Response::builder();
+ for (key, value) in client_resp.headers() {
+ server_resp
+ .headers_mut()
+ .expect("no errors could be in ResponseBuilder")
+ .append(key, value.clone());
+ }
+ Ok(server_resp.body(client_resp.into_body())?)
+}
+
+fn http_not_found(_request: Request) -> Response {
+ Response::builder()
+ .status(StatusCode::NOT_FOUND)
+ .body(Body::empty())
+ .unwrap()
+}
diff --git a/test-programs/src/lib.rs b/test-programs/src/lib.rs
index 86e6d9f..0d4a83f 100644
--- a/test-programs/src/lib.rs
+++ b/test-programs/src/lib.rs
@@ -6,6 +6,8 @@ use std::process::{Child, Command};
use std::thread::sleep;
use std::time::Duration;
+const DEFAULT_SERVER_PORT: u16 = 8081;
+
/// Manages exclusive access to port 8081, and kills the process when dropped
pub struct WasmtimeServe {
#[expect(dead_code, reason = "exists to live for as long as wasmtime process")]
@@ -22,26 +24,35 @@ impl WasmtimeServe {
///
/// Kills the wasmtime process, and releases the lock, once dropped.
pub fn new(guest: &str) -> std::io::Result {
+ Self::new_with_config(guest, DEFAULT_SERVER_PORT, &[])
+ }
+
+ pub fn new_with_config(guest: &str, port: u16, env_vars: &[&str]) -> std::io::Result {
let mut lockfile = std::env::temp_dir();
- lockfile.push("TEST_PROGRAMS_WASMTIME_SERVE.lock");
+ lockfile.push(format!("TEST_PROGRAMS_WASMTIME_SERVE_{port}.lock"));
let lockfile = File::create(&lockfile)?;
lockfile.lock()?;
// Run wasmtime serve.
// Enable -Scli because we currently don't have a way to build with the
// proxy adapter, so we build with the default adapter.
- let process = Command::new("wasmtime")
+ let mut process = Command::new("wasmtime");
+ let listening_addr = format!("127.0.0.1:{port}");
+ process
.arg("serve")
.arg("-Scli")
- .arg("--addr=127.0.0.1:8081")
- .arg(guest)
- .spawn()?;
+ .arg("--addr")
+ .arg(&listening_addr);
+ for env_var in env_vars {
+ process.arg("--env").arg(env_var);
+ }
+ let process = process.arg(guest).spawn()?;
let w = WasmtimeServe { lockfile, process };
// Clumsily wait for the server to accept connections.
'wait: loop {
sleep(Duration::from_millis(100));
- if TcpStream::connect("127.0.0.1:8081").is_ok() {
+ if TcpStream::connect(&listening_addr).is_ok() {
break 'wait;
}
}
diff --git a/test-programs/tests/http_server_proxy.rs b/test-programs/tests/http_server_proxy.rs
new file mode 100644
index 0000000..705e2e0
--- /dev/null
+++ b/test-programs/tests/http_server_proxy.rs
@@ -0,0 +1,20 @@
+use anyhow::Result;
+
+#[test_log::test]
+fn http_server_proxy() -> Result<()> {
+ // Run wasmtime serve for the proxy and the target HTTP server.
+ let _serve_target = test_programs::WasmtimeServe::new(test_programs::HTTP_SERVER)?;
+ let _serve_proxy = test_programs::WasmtimeServe::new_with_config(
+ test_programs::HTTP_SERVER_PROXY,
+ 8082,
+ &["TARGET_URL=http://127.0.0.1:8081"],
+ )?;
+
+ // TEST / of the `http_server` example through the proxy
+ let body: String = ureq::get("http://127.0.0.1:8082/proxy/")
+ .call()?
+ .body_mut()
+ .read_to_string()?;
+ assert_eq!(body, "Hello, wasi:http/proxy world!\n");
+ Ok(())
+}