From 4f49910ba93dbb626d2fe512209b2ce55a93cabd Mon Sep 17 00:00:00 2001 From: Tomas Olvecky Date: Tue, 25 Feb 2025 09:34:08 +0100 Subject: [PATCH 1/9] Add a http server proxy example --- examples/http_server_proxy.rs | 90 +++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 examples/http_server_proxy.rs diff --git a/examples/http_server_proxy.rs b/examples/http_server_proxy.rs new file mode 100644 index 0000000..26ac710 --- /dev/null +++ b/examples/http_server_proxy.rs @@ -0,0 +1,90 @@ +// Run the example with: +// wasmtime serve -Scli -Shttp --env TARGET_URL=https://example.com http_server_proxy.wasm +use futures_concurrency::prelude::*; +use wstd::http::body::{BodyForthcoming, IncomingBody}; +use wstd::http::server::{Finished, Responder}; +use wstd::http::{Client, Request, Response, StatusCode, Uri}; +use wstd::io::{copy, empty}; + +const PROXY_PREFIX: &str = "/proxy"; + +#[wstd::http_server] +async fn main(mut server_req: Request, responder: Responder) -> Finished { + 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"); + + 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 request to the client request. + for (key, value) in server_req.headers() { + client_req = client_req.header(key, value); + } + + // Send the request. + let client_req = client_req + .body(BodyForthcoming) + .expect("client_req.body failed"); + let (mut client_request_body, client_resp) = client + .start_request(client_req) + .await + .expect("client.start_request failed"); + + // Copy the server request body to client's request body. + let server_req_to_client_req = async { + let res = copy(server_req.body_mut(), &mut client_request_body).await; + // TODO: Convert to io error if necessary + let _ = Client::finish(client_request_body, None); + res + }; + + // Copy the client response headers to server response. + let client_resp_to_server_resp = async { + let client_resp = client_resp.await.unwrap(); + let mut server_resp = Response::builder(); + for (key, value) in client_resp.headers() { + server_resp + .headers_mut() + .unwrap() + .append(key, value.clone()); + } + // Start sending the server response. + let server_resp = server_resp.body(BodyForthcoming).unwrap(); + let mut server_resp = responder.start_response(server_resp); + + ( + copy(client_resp.into_body(), &mut server_resp).await, + server_resp, + ) + }; + + let (server_req_to_client_req, (client_resp_to_server_resp, server_resp)) = + (server_req_to_client_req, client_resp_to_server_resp) + .join() + .await; + let is_success = server_req_to_client_req.and(client_resp_to_server_resp); + Finished::finish(server_resp, is_success, None) + } + _ => http_not_found(server_req, responder).await, + } +} + +async fn http_not_found(_request: Request, responder: Responder) -> Finished { + let response = Response::builder() + .status(StatusCode::NOT_FOUND) + .body(empty()) + .unwrap(); + responder.respond(response).await +} From 1af8d332cf4a37eddaa561a88e6e3e69c82cf7b9 Mon Sep 17 00:00:00 2001 From: Tomas Olvecky Date: Thu, 13 Mar 2025 10:25:14 +0100 Subject: [PATCH 2/9] Propagate the `Client.finish` error to the proxy server's response --- examples/http_server_proxy.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/http_server_proxy.rs b/examples/http_server_proxy.rs index 26ac710..742f4be 100644 --- a/examples/http_server_proxy.rs +++ b/examples/http_server_proxy.rs @@ -1,12 +1,14 @@ // Run the example with: -// wasmtime serve -Scli -Shttp --env TARGET_URL=https://example.com http_server_proxy.wasm +// 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 +// Test with `curl --no-buffer -v 127.0.0.1:8080/proxy/` use futures_concurrency::prelude::*; use wstd::http::body::{BodyForthcoming, IncomingBody}; use wstd::http::server::{Finished, Responder}; use wstd::http::{Client, Request, Response, StatusCode, Uri}; use wstd::io::{copy, empty}; -const PROXY_PREFIX: &str = "/proxy"; +const PROXY_PREFIX: &str = "/proxy/"; #[wstd::http_server] async fn main(mut server_req: Request, responder: Responder) -> Finished { @@ -23,6 +25,7 @@ async fn main(mut server_req: Request, responder: Responder) -> Fi ) .parse() .expect("final target url should be parseable"); + println!("Proxying to {target_url}"); let client = Client::new(); let mut client_req = Request::builder(); @@ -45,9 +48,14 @@ async fn main(mut server_req: Request, responder: Responder) -> Fi // Copy the server request body to client's request body. let server_req_to_client_req = async { let res = copy(server_req.body_mut(), &mut client_request_body).await; - // TODO: Convert to io error if necessary - let _ = Client::finish(client_request_body, None); - res + Client::finish(client_request_body, None) + .map_err(|_http_err| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Failed to read HTTP request body", + ) + }) + .and(res) }; // Copy the client response headers to server response. From 884df595a2a79b830cd34e3d39950e14c0372291 Mon Sep 17 00:00:00 2001 From: Tomas Olvecky Date: Thu, 13 Mar 2025 10:34:30 +0100 Subject: [PATCH 3/9] Add a simplified reverse proxy example --- examples/http_server_proxy_simple.rs | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 examples/http_server_proxy_simple.rs diff --git a/examples/http_server_proxy_simple.rs b/examples/http_server_proxy_simple.rs new file mode 100644 index 0000000..a6f7f7c --- /dev/null +++ b/examples/http_server_proxy_simple.rs @@ -0,0 +1,40 @@ +// Run the example with: +// cargo build --example http_server_proxy_simple --target=wasm32-wasip2 +// wasmtime serve -Scli -Shttp --env TARGET_URL=https://example.com target/wasm32-wasip2/debug/examples/http_server_proxy_simple.wasm +// Test with `curl -v 127.0.0.1:8080` +use wstd::http::body::IncomingBody; +use wstd::http::server::{Finished, Responder}; +use wstd::http::{Client, Request, Response, Uri}; + +#[wstd::http_server] +async fn main(server_req: Request, responder: Responder) -> Finished { + let api_prefixed_path = server_req.uri().path_and_query().unwrap().as_str(); + let target_url = std::env::var("TARGET_URL").expect("missing environment variable TARGET_URL"); + let target_url: Uri = format!("{target_url}{}", api_prefixed_path) + .parse() + .expect("final target url should be parseable"); + println!("Proxying to {target_url}"); + + 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 request to the client request. + let (server_req_parts, server_req_body) = server_req.into_parts(); + *client_req.headers_mut().unwrap() = server_req_parts.headers; + // Send the whole request. + let client_req = client_req + .body(server_req_body) + .expect("client_req.body failed"); + + let client_resp: Response = + client.send(client_req).await.expect("client.send failed"); + let mut server_resp = Response::builder(); + let (client_resp_parts, client_resp_body) = client_resp.into_parts(); + *server_resp.headers_mut().unwrap() = client_resp_parts.headers; + // Send the response. + let server_resp = server_resp + .body(client_resp_body) + .expect("server_resp.body failed"); + responder.respond(server_resp).await +} From 4e1b60652f959193b63b7fa4612b7fc61790d785 Mon Sep 17 00:00:00 2001 From: Tomas Olvecky Date: Thu, 13 Mar 2025 10:46:17 +0100 Subject: [PATCH 4/9] Fix a typo --- examples/http_server_proxy.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/http_server_proxy.rs b/examples/http_server_proxy.rs index 742f4be..a76162c 100644 --- a/examples/http_server_proxy.rs +++ b/examples/http_server_proxy.rs @@ -52,7 +52,7 @@ async fn main(mut server_req: Request, responder: Responder) -> Fi .map_err(|_http_err| { std::io::Error::new( std::io::ErrorKind::InvalidData, - "Failed to read HTTP request body", + "Failed write the HTTP request body", ) }) .and(res) From 0c2b8ab54278d65120d70ac427d9876f5cb7fbb3 Mon Sep 17 00:00:00 2001 From: Tomas Olvecky Date: Sat, 25 Oct 2025 11:16:58 +0200 Subject: [PATCH 5/9] wip: Partially the proxy example to `wstd` --- examples/http_server_proxy.rs | 105 ++++++++++----------------- examples/http_server_proxy_simple.rs | 40 ---------- 2 files changed, 37 insertions(+), 108 deletions(-) delete mode 100644 examples/http_server_proxy_simple.rs diff --git a/examples/http_server_proxy.rs b/examples/http_server_proxy.rs index a76162c..99c6081 100644 --- a/examples/http_server_proxy.rs +++ b/examples/http_server_proxy.rs @@ -2,16 +2,14 @@ // 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 // Test with `curl --no-buffer -v 127.0.0.1:8080/proxy/` -use futures_concurrency::prelude::*; -use wstd::http::body::{BodyForthcoming, IncomingBody}; -use wstd::http::server::{Finished, Responder}; -use wstd::http::{Client, Request, Response, StatusCode, Uri}; -use wstd::io::{copy, empty}; + +use wstd::http::body::{Body, Bytes}; +use wstd::http::{Client, Error, Request, Response, StatusCode, Uri}; const PROXY_PREFIX: &str = "/proxy/"; #[wstd::http_server] -async fn main(mut server_req: Request, responder: Responder) -> Finished { +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 @@ -26,73 +24,44 @@ async fn main(mut server_req: Request, responder: Responder) -> Fi .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)), + } +} - 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 request to the client request. - for (key, value) in server_req.headers() { - client_req = client_req.header(key, value); - } - - // Send the request. - let client_req = client_req - .body(BodyForthcoming) - .expect("client_req.body failed"); - let (mut client_request_body, client_resp) = client - .start_request(client_req) - .await - .expect("client.start_request failed"); - - // Copy the server request body to client's request body. - let server_req_to_client_req = async { - let res = copy(server_req.body_mut(), &mut client_request_body).await; - Client::finish(client_request_body, None) - .map_err(|_http_err| { - std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Failed write the HTTP request body", - ) - }) - .and(res) - }; - - // Copy the client response headers to server response. - let client_resp_to_server_resp = async { - let client_resp = client_resp.await.unwrap(); - let mut server_resp = Response::builder(); - for (key, value) in client_resp.headers() { - server_resp - .headers_mut() - .unwrap() - .append(key, value.clone()); - } - // Start sending the server response. - let server_resp = server_resp.body(BodyForthcoming).unwrap(); - let mut server_resp = responder.start_response(server_resp); +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(client_resp.into_body(), &mut server_resp).await, - server_resp, - ) - }; + // Copy headers from `server_req` to the `client_req`. + for (key, value) in server_req.headers() { + client_req = client_req.header(key, value); + } - let (server_req_to_client_req, (client_resp_to_server_resp, server_resp)) = - (server_req_to_client_req, client_resp_to_server_resp) - .join() - .await; - let is_success = server_req_to_client_req.and(client_resp_to_server_resp); - Finished::finish(server_resp, is_success, None) - } - _ => http_not_found(server_req, responder).await, + // Stream the request body. + let client_body = Body::from_http_body(server_req.into_body().into_boxed_body()); + let client_req = client_req.body(client_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()); } + // FIXME: Convert UnsyncBoxBody to Response + server_resp + .body(client_resp.into_body().into_boxed_body()) + .map_err(Error::from) } -async fn http_not_found(_request: Request, responder: Responder) -> Finished { - let response = Response::builder() +fn http_not_found(_request: Request) -> Response { + Response::builder() .status(StatusCode::NOT_FOUND) - .body(empty()) - .unwrap(); - responder.respond(response).await + .body(Body::empty()) + .unwrap() } diff --git a/examples/http_server_proxy_simple.rs b/examples/http_server_proxy_simple.rs deleted file mode 100644 index a6f7f7c..0000000 --- a/examples/http_server_proxy_simple.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Run the example with: -// cargo build --example http_server_proxy_simple --target=wasm32-wasip2 -// wasmtime serve -Scli -Shttp --env TARGET_URL=https://example.com target/wasm32-wasip2/debug/examples/http_server_proxy_simple.wasm -// Test with `curl -v 127.0.0.1:8080` -use wstd::http::body::IncomingBody; -use wstd::http::server::{Finished, Responder}; -use wstd::http::{Client, Request, Response, Uri}; - -#[wstd::http_server] -async fn main(server_req: Request, responder: Responder) -> Finished { - let api_prefixed_path = server_req.uri().path_and_query().unwrap().as_str(); - let target_url = std::env::var("TARGET_URL").expect("missing environment variable TARGET_URL"); - let target_url: Uri = format!("{target_url}{}", api_prefixed_path) - .parse() - .expect("final target url should be parseable"); - println!("Proxying to {target_url}"); - - 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 request to the client request. - let (server_req_parts, server_req_body) = server_req.into_parts(); - *client_req.headers_mut().unwrap() = server_req_parts.headers; - // Send the whole request. - let client_req = client_req - .body(server_req_body) - .expect("client_req.body failed"); - - let client_resp: Response = - client.send(client_req).await.expect("client.send failed"); - let mut server_resp = Response::builder(); - let (client_resp_parts, client_resp_body) = client_resp.into_parts(); - *server_resp.headers_mut().unwrap() = client_resp_parts.headers; - // Send the response. - let server_resp = server_resp - .body(client_resp_body) - .expect("server_resp.body failed"); - responder.respond(server_resp).await -} From e653a1f72e821aaa5f54f64167c2b2049df65a5e Mon Sep 17 00:00:00 2001 From: Tomas Olvecky Date: Wed, 29 Oct 2025 20:52:57 +0100 Subject: [PATCH 6/9] Finish the streaming proxy example --- examples/http_server_proxy.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/http_server_proxy.rs b/examples/http_server_proxy.rs index 99c6081..074a759 100644 --- a/examples/http_server_proxy.rs +++ b/examples/http_server_proxy.rs @@ -3,7 +3,7 @@ // wasmtime serve -Scli -Shttp --env TARGET_URL=https://example.com/ target/wasm32-wasip2/debug/examples/http_server_proxy.wasm // Test with `curl --no-buffer -v 127.0.0.1:8080/proxy/` -use wstd::http::body::{Body, Bytes}; +use wstd::http::body::Body; use wstd::http::{Client, Error, Request, Response, StatusCode, Uri}; const PROXY_PREFIX: &str = "/proxy/"; @@ -53,10 +53,12 @@ async fn proxy(server_req: Request, target_url: Uri) -> Result to Response - server_resp + let resp_body = server_resp .body(client_resp.into_body().into_boxed_body()) - .map_err(Error::from) + .map_err(Error::from)?; + let (resp_parts, resp_body) = resp_body.into_parts(); + let resp_body = Body::from_http_body(resp_body); + Ok(Response::from_parts(resp_parts, resp_body)) } fn http_not_found(_request: Request) -> Response { From 04395ab0d65108ef2b1fbce957f93c694d12feb1 Mon Sep 17 00:00:00 2001 From: Tomas Olvecky Date: Thu, 30 Oct 2025 06:56:46 +0100 Subject: [PATCH 7/9] Simplify the response creation --- examples/http_server_proxy.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/examples/http_server_proxy.rs b/examples/http_server_proxy.rs index 074a759..3b95416 100644 --- a/examples/http_server_proxy.rs +++ b/examples/http_server_proxy.rs @@ -1,7 +1,7 @@ // Run the example with: // 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 -// Test with `curl --no-buffer -v 127.0.0.1:8080/proxy/` +// curl --no-buffer -v 127.0.0.1:8080/proxy/ use wstd::http::body::Body; use wstd::http::{Client, Error, Request, Response, StatusCode, Uri}; @@ -53,12 +53,8 @@ async fn proxy(server_req: Request, target_url: Uri) -> Result) -> Response { From b0db3158c8baccd950990f2a23906a25bf1d5c7f Mon Sep 17 00:00:00 2001 From: Tomas Olvecky Date: Mon, 3 Nov 2025 19:32:14 +0100 Subject: [PATCH 8/9] Skip copying request/response body contents through guest memory --- examples/http_server_proxy.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/http_server_proxy.rs b/examples/http_server_proxy.rs index 3b95416..2d709a2 100644 --- a/examples/http_server_proxy.rs +++ b/examples/http_server_proxy.rs @@ -41,8 +41,7 @@ async fn proxy(server_req: Request, target_url: Uri) -> Result, target_url: Uri) -> Result) -> Response { From 0be8bd6a405ce1f57a2660ee73000a00ede04373 Mon Sep 17 00:00:00 2001 From: Tomas Olvecky Date: Tue, 4 Nov 2025 13:16:59 +0100 Subject: [PATCH 9/9] Add `http_server_proxy` test --- examples/http_server_proxy.rs | 11 ++++++----- test-programs/src/lib.rs | 23 +++++++++++++++++------ test-programs/tests/http_server_proxy.rs | 20 ++++++++++++++++++++ 3 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 test-programs/tests/http_server_proxy.rs diff --git a/examples/http_server_proxy.rs b/examples/http_server_proxy.rs index 2d709a2..ccad608 100644 --- a/examples/http_server_proxy.rs +++ b/examples/http_server_proxy.rs @@ -1,8 +1,9 @@ -// Run the example with: -// 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/ - +//! 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}; 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(()) +}