Skip to content

Add a http server proxy example #66

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ serde_json = { workspace = true, optional = true }
[dev-dependencies]
anyhow.workspace = true
clap.workspace = true
futures-concurrency.workspace = true
futures-lite.workspace = true
humantime.workspace = true
serde = { workspace = true, features = ["derive"] }
Expand Down Expand Up @@ -61,6 +62,7 @@ authors = [
anyhow = "1"
cargo_metadata = "0.18.1"
clap = { version = "4.5.26", features = ["derive"] }
futures-concurrency = "7.6.3"
futures-core = "0.3.19"
futures-lite = "1.12.0"
humantime = "2.1.0"
Expand Down
98 changes: 98 additions & 0 deletions examples/http_server_proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// 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/`
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<IncomingBody>, 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");
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.
for (key, value) in server_req.headers() {
client_req = client_req.header(key, value);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of copying the headers one at a time, could this do what the http server example does?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the simplified example.


// 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 link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using start_request works here, though is a little more verbose than needed for a simple proxy. If you change the request above to use .body(server_req.into_body()), then you can use plain send instead of start_request and manually copying the body.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the simplified example.


// 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());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And similarly, could this avoid copying headers individually?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the simplified example.

// Start sending the server response.
let server_resp = server_resp.body(BodyForthcoming).unwrap();
let mut server_resp = responder.start_response(server_resp);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above, this can do server_resp.body(client_resp.into_body()) and plain respond.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the simplified example.


(
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<IncomingBody>, responder: Responder) -> Finished {
let response = Response::builder()
.status(StatusCode::NOT_FOUND)
.body(empty())
.unwrap();
responder.respond(response).await
}
40 changes: 40 additions & 0 deletions examples/http_server_proxy_simple.rs
Original file line number Diff line number Diff line change
@@ -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<IncomingBody>, 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<IncomingBody> =
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
}