Repository: bytecodealliance/wasmtime
Pull request #7545 (2023-11-15 19:12:10)

wasi-http: Populate the authority and scheme in `wasmtime serve`
Ensure that the scheme and authority are populated in the requests given to the incoming handler with `wasmtime serve`. We prefer the `Host` header if it's available, falling back on the existing value of the `authority` in the request. Additionally, we fall back on `http` as the scheme when none was present. This behavior mirrors what https://github.com/fastly/vicroy does for request handling, and ensures that runtimes that require an incoming authority to be set (like the javascript runtime) have one available.

cc @tschneidereit @JakeChampion 
<!--
Please make sure you include the following information:

- If this work has been discussed elsewhere, please include a link to that
  conversation. If it was discussed in an issue, just mention "issue #...".

- Explain why this change is needed. If the details are in an issue already,
  this can be brief.

Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.html

Please ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 2661c1154..3f698546e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3322,6 +3322,7 @@ dependencies = [
  "criterion",
  "env_logger 0.10.0",
  "filecheck",
+ "http",
  "http-body-util",
  "humantime 2.1.0",
  "hyper",
diff --git a/Cargo.toml b/Cargo.toml
index e13b6b7b0..bae7248d5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -54,6 +54,7 @@ async-trait = { workspace = true }
 bytes = { workspace = true }
 tokio = { workspace = true, optional = true, features = [ "signal", "macros" ] }
 hyper = { workspace = true, optional = true }
+http = { workspace = true, optional = true }
 http-body-util = { workspace = true, optional = true }
 
 [target.'cfg(unix)'.dependencies]
@@ -359,7 +360,7 @@ old-cli = []
 
 # CLI subcommands for the `wasmtime` executable. See `wasmtime $cmd --help`
 # for more information on each subcommand.
-serve = ["wasi-http", "component-model", "dep:http-body-util"]
+serve = ["wasi-http", "component-model", "dep:http-body-util", "dep:http"]
 explore = ["dep:wasmtime-explorer"]
 wast = ["dep:wasmtime-wast"]
 config = ["cache"]
diff --git a/src/commands/serve.rs b/src/commands/serve.rs
index fe5a8f240..2ce05a0e1 100644
--- a/src/commands/serve.rs
+++ b/src/commands/serve.rs
@@ -15,7 +15,8 @@ use wasmtime_wasi::preview2::{
     self, StreamError, StreamResult, Table, WasiCtx, WasiCtxBuilder, WasiView,
 };
 use wasmtime_wasi_http::{
-    body::HyperOutgoingBody, hyper_response_error, WasiHttpCtx, WasiHttpView,
+    bindings::http::types as http_types, body::HyperOutgoingBody, hyper_response_error,
+    WasiHttpCtx, WasiHttpView,
 };
 
 #[cfg(feature = "wasi-nn")]
@@ -356,6 +357,37 @@ impl hyper::service::Service<Request> for ProxyHandler {
         // TODO: need to track the join handle, but don't want to block the response on it
         tokio::task::spawn(async move {
             let req_id = inner.next_req_id();
+            let (mut parts, body) = req.into_parts();
+
+            parts.uri = {
+                let uri_parts = parts.uri.into_parts();
+
+                let scheme = uri_parts.scheme.unwrap_or(http::uri::Scheme::HTTP);
+
+                let host = if let Some(val) = parts.headers.get(hyper::header::HOST) {
+                    std::str::from_utf8(val.as_bytes())
+                        .map_err(|_| http_types::ErrorCode::HttpRequestUriInvalid)?
+                } else {
+                    uri_parts
+                        .authority
+                        .as_ref()
+                        .ok_or(http_types::ErrorCode::HttpRequestUriInvalid)?
+                        .host()
+                };
+
+                let path_with_query = uri_parts
+                    .path_and_query
+                    .ok_or(http_types::ErrorCode::HttpRequestUriInvalid)?;
+
+                hyper::Uri::builder()
+                    .scheme(scheme)
+                    .authority(host)
+                    .path_and_query(path_with_query)
+                    .build()
+                    .map_err(|_| http_types::ErrorCode::HttpRequestUriInvalid)?
+            };
+
+            let req = hyper::Request::from_parts(parts, body.map_err(hyper_response_error).boxed());
 
             log::info!(
                 "Request {req_id} handling {} to {}",
@@ -365,10 +397,7 @@ impl hyper::service::Service<Request> for ProxyHandler {
 
             let mut store = inner.cmd.new_store(&inner.engine, req_id)?;
 
-            let req = store
-                .data_mut()
-                .new_incoming_request(req.map(|body| body.map_err(hyper_response_error).boxed()))?;
-
+            let req = store.data_mut().new_incoming_request(req)?;
             let out = store.data_mut().new_response_outparam(sender)?;
 
             let (proxy, _inst) =


Test patch:
diff --git a/crates/test-programs/src/bin/api_proxy.rs b/crates/test-programs/src/bin/api_proxy.rs
index d9ca3936f..33114699b 100644
--- a/crates/test-programs/src/bin/api_proxy.rs
+++ b/crates/test-programs/src/bin/api_proxy.rs
@@ -16,6 +16,10 @@ struct T;
 
 impl bindings::exports::wasi::http::incoming_handler::Guest for T {
     fn handle(request: IncomingRequest, outparam: ResponseOutparam) {
+        assert!(request.scheme().is_some());
+        assert!(request.authority().is_some());
+        assert!(request.path_with_query().is_some());
+
         let header = String::from("custom-forbidden-header");
         let req_hdrs = request.headers();
 
diff --git a/crates/test-programs/src/bin/api_proxy_streaming.rs b/crates/test-programs/src/bin/api_proxy_streaming.rs
index b2a354650..d03ff3623 100644
--- a/crates/test-programs/src/bin/api_proxy_streaming.rs
+++ b/crates/test-programs/src/bin/api_proxy_streaming.rs
@@ -33,6 +33,8 @@ impl bindings::exports::wasi::http::incoming_handler::Guest for Handler {
 async fn handle_request(request: IncomingRequest, response_out: ResponseOutparam) {
     let headers = request.headers().entries();
 
+    assert!(request.authority().is_some());
+
     match (request.method(), request.path_with_query().as_deref()) {
         (Method::Get, Some("/hash-all")) => {
             // Send outgoing GET requests to the specified URLs and stream the hashes of the response bodies as
diff --git a/crates/wasi-http/tests/all/main.rs b/crates/wasi-http/tests/all/main.rs
index 323d41892..0479161f8 100644
--- a/crates/wasi-http/tests/all/main.rs
+++ b/crates/wasi-http/tests/all/main.rs
@@ -199,11 +199,10 @@ async fn run_wasi_http(
 
 #[test_log::test(tokio::test)]
 async fn wasi_http_proxy_tests() -> anyhow::Result<()> {
-    let mut req = hyper::Request::builder().method(http::Method::GET);
-
-    req.headers_mut()
-        .unwrap()
-        .append("custom-forbidden-header", "yes".parse().unwrap());
+    let req = hyper::Request::builder()
+        .header("custom-forbidden-header", "yes")
+        .uri("http://example.com:8080/test-path")
+        .method(http::Method::GET);
 
     let resp = run_wasi_http(
         test_programs_artifacts::API_PROXY_COMPONENT,
@@ -326,7 +325,9 @@ async fn do_wasi_http_hash_all(override_send_request: bool) -> Result<()> {
         None
     };
 
-    let mut request = hyper::Request::get("/hash-all");
+    let mut request = hyper::Request::builder()
+        .method(http::Method::GET)
+        .uri("http://example.com:8080/hash-all");
     for path in bodies.keys() {
         request = request.header("url", format!("{prefix}{path}"));
     }
@@ -448,8 +449,10 @@ async fn do_wasi_http_echo(uri: &str, url_header: Option<&str>) -> Result<()> {
         .collect::<Vec<_>>()
     };
 
-    let mut request =
-        hyper::Request::post(&format!("/{uri}")).header("content-type", "application/octet-stream");
+    let mut request = hyper::Request::builder()
+        .method(http::Method::POST)
+        .uri(format!("http://example.com:8080/{uri}"))
+        .header("content-type", "application/octet-stream");
 
     if let Some(url_header) = url_header {
         request = request.header("url", url_header);