Skip to content

Commit 6c14d5a

Browse files
authored
Add a http server proxy example (#66)
* Add a http server proxy example * Propagate the `Client.finish` error to the proxy server's response * Add a simplified reverse proxy example * Fix a typo * wip: Partially the proxy example to `wstd` * Finish the streaming proxy example * Simplify the response creation * Skip copying request/response body contents through guest memory * Add `http_server_proxy` test
1 parent e836a9c commit 6c14d5a

File tree

3 files changed

+101
-6
lines changed

3 files changed

+101
-6
lines changed

examples/http_server_proxy.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//! Run the example with:
2+
//! ```sh
3+
//! cargo build --example http_server_proxy --target=wasm32-wasip2
4+
//! wasmtime serve -Scli -Shttp --env TARGET_URL=https://example.com/ target/wasm32-wasip2/debug/examples/http_server_proxy.wasm
5+
//! curl --no-buffer -v 127.0.0.1:8080/proxy/
6+
//! ```
7+
use wstd::http::body::Body;
8+
use wstd::http::{Client, Error, Request, Response, StatusCode, Uri};
9+
10+
const PROXY_PREFIX: &str = "/proxy/";
11+
12+
#[wstd::http_server]
13+
async fn main(server_req: Request<Body>) -> Result<Response<Body>, Error> {
14+
match server_req.uri().path_and_query().unwrap().as_str() {
15+
api_prefixed_path if api_prefixed_path.starts_with(PROXY_PREFIX) => {
16+
// Remove PROXY_PREFIX
17+
let target_url =
18+
std::env::var("TARGET_URL").expect("missing environment variable TARGET_URL");
19+
let target_url: Uri = format!(
20+
"{target_url}{}",
21+
api_prefixed_path
22+
.strip_prefix(PROXY_PREFIX)
23+
.expect("checked above")
24+
)
25+
.parse()
26+
.expect("final target url should be parseable");
27+
println!("Proxying to {target_url}");
28+
proxy(server_req, target_url).await
29+
}
30+
_ => Ok(http_not_found(server_req)),
31+
}
32+
}
33+
34+
async fn proxy(server_req: Request<Body>, target_url: Uri) -> Result<Response<Body>, Error> {
35+
let client = Client::new();
36+
let mut client_req = Request::builder();
37+
client_req = client_req.uri(target_url).method(server_req.method());
38+
39+
// Copy headers from `server_req` to the `client_req`.
40+
for (key, value) in server_req.headers() {
41+
client_req = client_req.header(key, value);
42+
}
43+
44+
// Stream the request body.
45+
let client_req = client_req.body(server_req.into_body())?;
46+
// Send the request.
47+
let client_resp = client.send(client_req).await?;
48+
// Copy headers from `client_resp` to `server_resp`.
49+
let mut server_resp = Response::builder();
50+
for (key, value) in client_resp.headers() {
51+
server_resp
52+
.headers_mut()
53+
.expect("no errors could be in ResponseBuilder")
54+
.append(key, value.clone());
55+
}
56+
Ok(server_resp.body(client_resp.into_body())?)
57+
}
58+
59+
fn http_not_found(_request: Request<Body>) -> Response<Body> {
60+
Response::builder()
61+
.status(StatusCode::NOT_FOUND)
62+
.body(Body::empty())
63+
.unwrap()
64+
}

test-programs/src/lib.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use std::process::{Child, Command};
66
use std::thread::sleep;
77
use std::time::Duration;
88

9+
const DEFAULT_SERVER_PORT: u16 = 8081;
10+
911
/// Manages exclusive access to port 8081, and kills the process when dropped
1012
pub struct WasmtimeServe {
1113
#[expect(dead_code, reason = "exists to live for as long as wasmtime process")]
@@ -22,26 +24,35 @@ impl WasmtimeServe {
2224
///
2325
/// Kills the wasmtime process, and releases the lock, once dropped.
2426
pub fn new(guest: &str) -> std::io::Result<Self> {
27+
Self::new_with_config(guest, DEFAULT_SERVER_PORT, &[])
28+
}
29+
30+
pub fn new_with_config(guest: &str, port: u16, env_vars: &[&str]) -> std::io::Result<Self> {
2531
let mut lockfile = std::env::temp_dir();
26-
lockfile.push("TEST_PROGRAMS_WASMTIME_SERVE.lock");
32+
lockfile.push(format!("TEST_PROGRAMS_WASMTIME_SERVE_{port}.lock"));
2733
let lockfile = File::create(&lockfile)?;
2834
lockfile.lock()?;
2935

3036
// Run wasmtime serve.
3137
// Enable -Scli because we currently don't have a way to build with the
3238
// proxy adapter, so we build with the default adapter.
33-
let process = Command::new("wasmtime")
39+
let mut process = Command::new("wasmtime");
40+
let listening_addr = format!("127.0.0.1:{port}");
41+
process
3442
.arg("serve")
3543
.arg("-Scli")
36-
.arg("--addr=127.0.0.1:8081")
37-
.arg(guest)
38-
.spawn()?;
44+
.arg("--addr")
45+
.arg(&listening_addr);
46+
for env_var in env_vars {
47+
process.arg("--env").arg(env_var);
48+
}
49+
let process = process.arg(guest).spawn()?;
3950
let w = WasmtimeServe { lockfile, process };
4051

4152
// Clumsily wait for the server to accept connections.
4253
'wait: loop {
4354
sleep(Duration::from_millis(100));
44-
if TcpStream::connect("127.0.0.1:8081").is_ok() {
55+
if TcpStream::connect(&listening_addr).is_ok() {
4556
break 'wait;
4657
}
4758
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use anyhow::Result;
2+
3+
#[test_log::test]
4+
fn http_server_proxy() -> Result<()> {
5+
// Run wasmtime serve for the proxy and the target HTTP server.
6+
let _serve_target = test_programs::WasmtimeServe::new(test_programs::HTTP_SERVER)?;
7+
let _serve_proxy = test_programs::WasmtimeServe::new_with_config(
8+
test_programs::HTTP_SERVER_PROXY,
9+
8082,
10+
&["TARGET_URL=http://127.0.0.1:8081"],
11+
)?;
12+
13+
// TEST / of the `http_server` example through the proxy
14+
let body: String = ureq::get("http://127.0.0.1:8082/proxy/")
15+
.call()?
16+
.body_mut()
17+
.read_to_string()?;
18+
assert_eq!(body, "Hello, wasi:http/proxy world!\n");
19+
Ok(())
20+
}

0 commit comments

Comments
 (0)