Repository: bytecodealliance/wasmtime
Pull request #7405 (2023-10-30 16:47:44)

Attempt to automatically configure release notes
This commit is an attempt to tackle #7068 by configuring the release notes in Github Releases with the handwritten release notes from `RELEASES.md`. The basic idea here is to split the markdown file on `-----` delimiters and then find the one which matches the version being released. Once one is found the `body` field of the API call to create the release is configured.

Closes https://github.com/bytecodealliance/wasmtime/issues/7068

alexcrichton: An example release is [here](https://github.com/alexcrichton/wasmtime/releases/tag/v15.0.0) which is where I tested this.

Patch:
diff --git a/.github/actions/github-release/main.js b/.github/actions/github-release/main.js
index d61b24ed5..bcf4f75b4 100644
--- a/.github/actions/github-release/main.js
+++ b/.github/actions/github-release/main.js
@@ -91,13 +91,25 @@ async function runOnce() {
   } catch (e) {
     console.log("ERROR: ", JSON.stringify(e, null, 2));
     core.info(`creating a release`);
+
+    const releaseNotes = fs.readFileSync('RELEASES.md').toString();
+    let notes = null;
+    const opts = {
+      owner,
+      repo,
+      tag_name: name,
+      prerelease: name === 'dev',
+    };
+    if (name !== 'dev') {
+      for (let x of releaseNotes.split(/^---+$/m)) {
+        if (x.indexOf(name.substring(1)) == -1)
+          continue;
+        opts.body = x;
+        break;
+      }
+    }
     try {
-      release = await octokit.rest.repos.createRelease({
-        owner,
-        repo,
-        tag_name: name,
-        prerelease: name === 'dev',
-      });
+      release = await octokit.rest.repos.createRelease(opts);
     } catch(e) {
       console.log("ERROR: ", JSON.stringify(e, null, 2));
       core.info(`fetching one more time`);
diff --git a/crates/wasi-http/src/http_impl.rs b/crates/wasi-http/src/http_impl.rs
index d9e25a4ce..f77575c9c 100644
--- a/crates/wasi-http/src/http_impl.rs
+++ b/crates/wasi-http/src/http_impl.rs
@@ -65,15 +65,22 @@ impl<T: WasiHttpView> outgoing_handler::Host for T {
             }
         };
 
-        let authority = if req.authority.find(':').is_some() {
-            req.authority.clone()
+        let authority = if let Some(authority) = req.authority {
+            if authority.find(':').is_some() {
+                authority
+            } else {
+                format!("{}:{port}", authority)
+            }
         } else {
-            format!("{}:{port}", req.authority)
+            String::new()
         };
 
         let mut builder = hyper::Request::builder()
             .method(method)
-            .uri(format!("{scheme}{authority}{}", req.path_with_query))
+            .uri(format!(
+                "{scheme}{authority}{}",
+                req.path_with_query.as_ref().map_or("", String::as_ref)
+            ))
             .header(hyper::header::HOST, &authority);
 
         for (k, v) in req.headers.iter() {
diff --git a/crates/wasi-http/src/types.rs b/crates/wasi-http/src/types.rs
index 76530811c..50119b989 100644
--- a/crates/wasi-http/src/types.rs
+++ b/crates/wasi-http/src/types.rs
@@ -193,6 +193,51 @@ fn invalid_url(e: std::io::Error) -> anyhow::Error {
     ))
 }
 
+impl From<http::Method> for types::Method {
+    fn from(method: http::Method) -> Self {
+        if method == http::Method::GET {
+            types::Method::Get
+        } else if method == hyper::Method::HEAD {
+            types::Method::Head
+        } else if method == hyper::Method::POST {
+            types::Method::Post
+        } else if method == hyper::Method::PUT {
+            types::Method::Put
+        } else if method == hyper::Method::DELETE {
+            types::Method::Delete
+        } else if method == hyper::Method::CONNECT {
+            types::Method::Connect
+        } else if method == hyper::Method::OPTIONS {
+            types::Method::Options
+        } else if method == hyper::Method::TRACE {
+            types::Method::Trace
+        } else if method == hyper::Method::PATCH {
+            types::Method::Patch
+        } else {
+            types::Method::Other(method.to_string())
+        }
+    }
+}
+
+impl TryInto<http::Method> for types::Method {
+    type Error = http::method::InvalidMethod;
+
+    fn try_into(self) -> Result<http::Method, Self::Error> {
+        match self {
+            Method::Get => Ok(http::Method::GET),
+            Method::Head => Ok(http::Method::HEAD),
+            Method::Post => Ok(http::Method::POST),
+            Method::Put => Ok(http::Method::PUT),
+            Method::Delete => Ok(http::Method::DELETE),
+            Method::Connect => Ok(http::Method::CONNECT),
+            Method::Options => Ok(http::Method::OPTIONS),
+            Method::Trace => Ok(http::Method::TRACE),
+            Method::Patch => Ok(http::Method::PATCH),
+            Method::Other(s) => http::Method::from_bytes(s.as_bytes()),
+        }
+    }
+}
+
 pub struct HostIncomingRequest {
     pub parts: http::request::Parts,
     pub body: Option<HostIncomingBodyBuilder>,
@@ -206,8 +251,8 @@ pub struct HostResponseOutparam {
 pub struct HostOutgoingRequest {
     pub method: Method,
     pub scheme: Option<Scheme>,
-    pub path_with_query: String,
-    pub authority: String,
+    pub path_with_query: Option<String>,
+    pub authority: Option<String>,
     pub headers: FieldMap,
     pub body: Option<HyperOutgoingBody>,
 }
diff --git a/crates/wasi-http/src/types_impl.rs b/crates/wasi-http/src/types_impl.rs
index dbaa54969..d1c8eabb6 100644
--- a/crates/wasi-http/src/types_impl.rs
+++ b/crates/wasi-http/src/types_impl.rs
@@ -1,4 +1,4 @@
-use crate::bindings::http::types::{Error, Headers, Method, Scheme, StatusCode, Trailers};
+use crate::bindings::http::types::{self, Error, Headers, Method, Scheme, StatusCode, Trailers};
 use crate::body::{FinishMessage, HostFutureTrailers, HostFutureTrailersState};
 use crate::types::{HostIncomingRequest, HostOutgoingResponse};
 use crate::WasiHttpView;
@@ -19,6 +19,19 @@ use wasmtime_wasi::preview2::{
 
 impl<T: WasiHttpView> crate::bindings::http::types::Host for T {}
 
+/// Take ownership of the underlying [`FieldMap`] associated with this fields resource. If the
+/// fields resource references another fields, the returned [`FieldMap`] will be cloned.
+fn move_fields(table: &mut Table, id: Resource<HostFields>) -> wasmtime::Result<FieldMap> {
+    match table.delete(id)? {
+        HostFields::Ref { parent, get_fields } => {
+            let entry = table.get_any_mut(parent)?;
+            Ok(get_fields(entry).clone())
+        }
+
+        HostFields::Owned { fields } => Ok(fields),
+    }
+}
+
 fn get_fields_mut<'a>(
     table: &'a mut Table,
     id: &Resource<HostFields>,
@@ -145,29 +158,8 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostFields for T {
 
 impl<T: WasiHttpView> crate::bindings::http::types::HostIncomingRequest for T {
     fn method(&mut self, id: Resource<HostIncomingRequest>) -> wasmtime::Result<Method> {
-        let method = self.table().get(&id)?.parts.method.as_ref();
-
-        if method == hyper::Method::GET {
-            Ok(Method::Get)
-        } else if method == hyper::Method::HEAD {
-            Ok(Method::Head)
-        } else if method == hyper::Method::POST {
-            Ok(Method::Post)
-        } else if method == hyper::Method::PUT {
-            Ok(Method::Put)
-        } else if method == hyper::Method::DELETE {
-            Ok(Method::Delete)
-        } else if method == hyper::Method::CONNECT {
-            Ok(Method::Connect)
-        } else if method == hyper::Method::OPTIONS {
-            Ok(Method::Options)
-        } else if method == hyper::Method::TRACE {
-            Ok(Method::Trace)
-        } else if method == hyper::Method::PATCH {
-            Ok(Method::Patch)
-        } else {
-            Ok(Method::Other(method.to_owned()))
-        }
+        let method = self.table().get(&id)?.parts.method.clone();
+        Ok(method.into())
     }
     fn path_with_query(
         &mut self,
@@ -258,11 +250,12 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostOutgoingRequest for T {
         authority: Option<String>,
         headers: Resource<Headers>,
     ) -> wasmtime::Result<Resource<HostOutgoingRequest>> {
-        let headers = get_fields_mut(self.table(), &headers)?.clone();
+        let headers = move_fields(self.table(), headers)?;
+
         self.table()
             .push(HostOutgoingRequest {
-                path_with_query: path_with_query.unwrap_or("".to_string()),
-                authority: authority.unwrap_or("".to_string()),
+                path_with_query,
+                authority,
                 method,
                 headers,
                 scheme,
@@ -271,7 +264,7 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostOutgoingRequest for T {
             .context("[new_outgoing_request] pushing request")
     }
 
-    fn write(
+    fn body(
         &mut self,
         request: Resource<HostOutgoingRequest>,
     ) -> wasmtime::Result<Result<Resource<HostOutgoingBody>, ()>> {
@@ -299,6 +292,97 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostOutgoingRequest for T {
         let _ = self.table().delete(request)?;
         Ok(())
     }
+
+    fn method(
+        &mut self,
+        request: wasmtime::component::Resource<types::OutgoingRequest>,
+    ) -> wasmtime::Result<Method> {
+        Ok(self.table().get(&request)?.method.clone().try_into()?)
+    }
+
+    fn set_method(
+        &mut self,
+        request: wasmtime::component::Resource<types::OutgoingRequest>,
+        method: Method,
+    ) -> wasmtime::Result<()> {
+        self.table().get_mut(&request)?.method = method.into();
+        Ok(())
+    }
+
+    fn path_with_query(
+        &mut self,
+        request: wasmtime::component::Resource<types::OutgoingRequest>,
+    ) -> wasmtime::Result<Option<String>> {
+        Ok(self.table().get(&request)?.path_with_query.clone())
+    }
+
+    fn set_path_with_query(
+        &mut self,
+        request: wasmtime::component::Resource<types::OutgoingRequest>,
+        path_with_query: Option<String>,
+    ) -> wasmtime::Result<()> {
+        self.table().get_mut(&request)?.path_with_query = path_with_query;
+        Ok(())
+    }
+
+    fn scheme(
+        &mut self,
+        request: wasmtime::component::Resource<types::OutgoingRequest>,
+    ) -> wasmtime::Result<Option<Scheme>> {
+        Ok(self.table().get(&request)?.scheme.clone())
+    }
+
+    fn set_scheme(
+        &mut self,
+        request: wasmtime::component::Resource<types::OutgoingRequest>,
+        scheme: Option<Scheme>,
+    ) -> wasmtime::Result<()> {
+        self.table().get_mut(&request)?.scheme = scheme;
+        Ok(())
+    }
+
+    fn authority(
+        &mut self,
+        request: wasmtime::component::Resource<types::OutgoingRequest>,
+    ) -> wasmtime::Result<Option<String>> {
+        Ok(self.table().get(&request)?.authority.clone())
+    }
+
+    fn set_authority(
+        &mut self,
+        request: wasmtime::component::Resource<types::OutgoingRequest>,
+        authority: Option<String>,
+    ) -> wasmtime::Result<()> {
+        self.table().get_mut(&request)?.authority = authority;
+        Ok(())
+    }
+
+    fn headers(
+        &mut self,
+        request: wasmtime::component::Resource<types::OutgoingRequest>,
+    ) -> wasmtime::Result<wasmtime::component::Resource<Headers>> {
+        let _ = self
+            .table()
+            .get(&request)
+            .context("[outgoing_request_headers] getting request")?;
+
+        fn get_fields(elem: &mut dyn Any) -> &mut FieldMap {
+            &mut elem
+                .downcast_mut::<types::OutgoingRequest>()
+                .unwrap()
+                .headers
+        }
+
+        let id = self.table().push_child(
+            HostFields::Ref {
+                parent: request.rep(),
+                get_fields,
+            },
+            &request,
+        )?;
+
+        Ok(id)
+    }
 }
 
 impl<T: WasiHttpView> crate::bindings::http::types::HostResponseOutparam for T {
@@ -470,7 +554,7 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostOutgoingResponse for T {
         status: StatusCode,
         headers: Resource<Headers>,
     ) -> wasmtime::Result<Resource<HostOutgoingResponse>> {
-        let fields = get_fields_mut(self.table(), &headers)?.clone();
+        let fields = move_fields(self.table(), headers)?;
 
         let id = self.table().push(HostOutgoingResponse {
             status,
@@ -481,7 +565,7 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostOutgoingResponse for T {
         Ok(id)
     }
 
-    fn write(
+    fn body(
         &mut self,
         id: Resource<HostOutgoingResponse>,
     ) -> wasmtime::Result<Result<Resource<HostOutgoingBody>, ()>> {
@@ -500,6 +584,43 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostOutgoingResponse for T {
         Ok(Ok(id))
     }
 
+    fn status_code(
+        &mut self,
+        id: Resource<HostOutgoingResponse>,
+    ) -> wasmtime::Result<types::StatusCode> {
+        Ok(self.table().get(&id)?.status)
+    }
+
+    fn set_status_code(
+        &mut self,
+        id: Resource<HostOutgoingResponse>,
+        status: types::StatusCode,
+    ) -> wasmtime::Result<()> {
+        self.table().get_mut(&id)?.status = status;
+        Ok(())
+    }
+
+    fn headers(
+        &mut self,
+        id: Resource<HostOutgoingResponse>,
+    ) -> wasmtime::Result<Resource<types::Headers>> {
+        // Trap if the outgoing-response doesn't exist.
+        let _ = self.table().get(&id)?;
+
+        fn get_fields(elem: &mut dyn Any) -> &mut FieldMap {
+            let resp = elem.downcast_mut::<HostOutgoingResponse>().unwrap();
+            &mut resp.headers
+        }
+
+        Ok(self.table().push_child(
+            HostFields::Ref {
+                parent: id.rep(),
+                get_fields,
+            },
+            &id,
+        )?)
+    }
+
     fn drop(&mut self, id: Resource<HostOutgoingResponse>) -> wasmtime::Result<()> {
         let _ = self.table().delete(id)?;
         Ok(())
diff --git a/crates/wasi-http/wit/deps/http/types.wit b/crates/wasi-http/wit/deps/http/types.wit
index 343d32220..9cace67ab 100644
--- a/crates/wasi-http/wit/deps/http/types.wit
+++ b/crates/wasi-http/wit/deps/http/types.wit
@@ -127,17 +127,73 @@ interface types {
   resource outgoing-request {
 
     /// Construct a new `outgoing-request`.
+    ///
+    /// * `method` represents the HTTP Method for the Request.
+    /// * `path-with-query` is the combination of the HTTP Path and Query for
+    ///    the Request. When `none`, this represents an empty Path and empty
+    ///    Query.
+    /// * `scheme` is the HTTP Related Scheme for the Request. When `none`,
+    ///    the implementation may choose an appropriate default scheme.
+    /// * `authority` is the HTTP Authority for the Request. A value of `none`
+    ///    may be used with Related Schemes which do not require an Authority.
+    ///    The HTTP and HTTPS schemes always require an authority.
+    /// * `headers` is the HTTP Headers for the Request.
+    ///
+    /// It is possible to construct, or manipulate with the accessor functions
+    /// below, an `outgoing-request` with an invalid combination of `scheme`
+    /// and `authority`, or `headers` which are not permitted to be sent.
+    /// It is the obligation of the `outgoing-handler.handle` implementation
+    /// to reject invalid constructions of `outgoing-request`.
     constructor(
       method: method,
       path-with-query: option<string>,
       scheme: option<scheme>,
       authority: option<string>,
-      headers: borrow<headers>
+      headers: headers
     );
 
-    /// Will return the outgoing-body child at most once. If called more than
-    /// once, subsequent calls will return error.
-    write: func() -> result<outgoing-body>;
+    /// Returns the resource corresponding to the outgoing Body for this
+    /// Request.
+    ///
+    /// Returns success on the first call: the `outgoing-body` resource for
+    /// this `outgoing-response` can be retrieved at most once. Subsequent
+    /// calls will return error.
+    body: func() -> result<outgoing-body>;
+
+    /// Get the Method for the Request.
+    method: func() -> method;
+    /// Set the Method for the Request.
+    set-method: func(method: method);
+
+    /// Get the combination of the HTTP Path and Query for the Request.
+    /// When `none`, this represents an empty Path and empty Query.
+    path-with-query: func() -> option<string>;
+    /// Set the combination of the HTTP Path and Query for the Request.
+    /// When `none`, this represents an empty Path and empty Query.
+    set-path-with-query: func(path-with-query: option<string>);
+
+    /// Get the HTTP Related Scheme for the Request. When `none`, the
+    /// implementation may choose an appropriate default scheme.
+    scheme: func() -> option<scheme>;
+    /// Set the HTTP Related Scheme for the Request. When `none`, the
+    /// implementation may choose an appropriate default scheme.
+    set-scheme: func(scheme: option<scheme>);
+
+    /// Get the HTTP Authority for the Request. A value of `none` may be used
+    /// with Related Schemes which do not require an Authority. The HTTP and
+    /// HTTPS schemes always require an authority.
+    authority: func() -> option<string>;
+    /// Set the HTTP Authority for the Request. A value of `none` may be used
+    /// with Related Schemes which do not require an Authority. The HTTP and
+    /// HTTPS schemes always require an authority.
+    set-authority: func(authority: option<string>);
+
+    /// Get the headers associated with the Request.
+    ///
+    /// This headers resource is a child: it must be dropped before the parent
+    /// `outgoing-request` is dropped, or its ownership is transfered to
+    /// another component by e.g. `outgoing-handler.handle`.
+    headers: func() -> headers;
   }
 
   /// Parameters for making an HTTP Request. Each of these parameters is an
@@ -176,6 +232,9 @@ interface types {
     /// This method consumes the `response-outparam` to ensure that it is
     /// called at most once. If it is never called, the implementation
     /// will respond with an error.
+    ///
+    /// The user may provide an `error` to `response` to allow the
+    /// implementation determine how to respond with an HTTP error response.
     set: static func(
       param: response-outparam,
       response: result<outgoing-response, error>,
@@ -260,16 +319,29 @@ interface types {
   resource outgoing-response {
 
     /// Construct an `outgoing-response`.
-    constructor(status-code: status-code, headers: borrow<headers>);
+    ///
+    /// * `status-code` is the HTTP Status Code for the Response.
+    /// * `headers` is the HTTP Headers for the Response.
+    constructor(status-code: status-code, headers: headers);
+
+    /// Get the HTTP Status Code for the Response.
+    status-code: func() -> status-code;
+    /// Set the HTTP Status Code for the Response.
+    set-status-code: func(status-code: status-code);
+
+    /// Get the headers associated with the Request.
+    ///
+    /// This headers resource is a child: it must be dropped before the parent
+    /// `outgoing-request` is dropped, or its ownership is transfered to
+    /// another component by e.g. `outgoing-handler.handle`.
+    headers: func() -> headers;
 
     /// Returns the resource corresponding to the outgoing Body for this Response.
     ///
     /// Returns success on the first call: the `outgoing-body` resource for
-    /// this `outgoing-response` can be retrieved at most once. Sunsequent
+    /// this `outgoing-response` can be retrieved at most once. Subsequent
     /// calls will return error.
-    ///
-    /// FIXME: rename this method to `body`.
-    write: func() -> result<outgoing-body>;
+    body: func() -> result<outgoing-body>;
   }
 
   /// Represents an outgoing HTTP Request or Response's Body.
diff --git a/crates/wasi/wit/deps/http/types.wit b/crates/wasi/wit/deps/http/types.wit
index 343d32220..9cace67ab 100644
--- a/crates/wasi/wit/deps/http/types.wit
+++ b/crates/wasi/wit/deps/http/types.wit
@@ -127,17 +127,73 @@ interface types {
   resource outgoing-request {
 
     /// Construct a new `outgoing-request`.
+    ///
+    /// * `method` represents the HTTP Method for the Request.
+    /// * `path-with-query` is the combination of the HTTP Path and Query for
+    ///    the Request. When `none`, this represents an empty Path and empty
+    ///    Query.
+    /// * `scheme` is the HTTP Related Scheme for the Request. When `none`,
+    ///    the implementation may choose an appropriate default scheme.
+    /// * `authority` is the HTTP Authority for the Request. A value of `none`
+    ///    may be used with Related Schemes which do not require an Authority.
+    ///    The HTTP and HTTPS schemes always require an authority.
+    /// * `headers` is the HTTP Headers for the Request.
+    ///
+    /// It is possible to construct, or manipulate with the accessor functions
+    /// below, an `outgoing-request` with an invalid combination of `scheme`
+    /// and `authority`, or `headers` which are not permitted to be sent.
+    /// It is the obligation of the `outgoing-handler.handle` implementation
+    /// to reject invalid constructions of `outgoing-request`.
     constructor(
       method: method,
       path-with-query: option<string>,
       scheme: option<scheme>,
       authority: option<string>,
-      headers: borrow<headers>
+      headers: headers
     );
 
-    /// Will return the outgoing-body child at most once. If called more than
-    /// once, subsequent calls will return error.
-    write: func() -> result<outgoing-body>;
+    /// Returns the resource corresponding to the outgoing Body for this
+    /// Request.
+    ///
+    /// Returns success on the first call: the `outgoing-body` resource for
+    /// this `outgoing-response` can be retrieved at most once. Subsequent
+    /// calls will return error.
+    body: func() -> result<outgoing-body>;
+
+    /// Get the Method for the Request.
+    method: func() -> method;
+    /// Set the Method for the Request.
+    set-method: func(method: method);
+
+    /// Get the combination of the HTTP Path and Query for the Request.
+    /// When `none`, this represents an empty Path and empty Query.
+    path-with-query: func() -> option<string>;
+    /// Set the combination of the HTTP Path and Query for the Request.
+    /// When `none`, this represents an empty Path and empty Query.
+    set-path-with-query: func(path-with-query: option<string>);
+
+    /// Get the HTTP Related Scheme for the Request. When `none`, the
+    /// implementation may choose an appropriate default scheme.
+    scheme: func() -> option<scheme>;
+    /// Set the HTTP Related Scheme for the Request. When `none`, the
+    /// implementation may choose an appropriate default scheme.
+    set-scheme: func(scheme: option<scheme>);
+
+    /// Get the HTTP Authority for the Request. A value of `none` may be used
+    /// with Related Schemes which do not require an Authority. The HTTP and
+    /// HTTPS schemes always require an authority.
+    authority: func() -> option<string>;
+    /// Set the HTTP Authority for the Request. A value of `none` may be used
+    /// with Related Schemes which do not require an Authority. The HTTP and
+    /// HTTPS schemes always require an authority.
+    set-authority: func(authority: option<string>);
+
+    /// Get the headers associated with the Request.
+    ///
+    /// This headers resource is a child: it must be dropped before the parent
+    /// `outgoing-request` is dropped, or its ownership is transfered to
+    /// another component by e.g. `outgoing-handler.handle`.
+    headers: func() -> headers;
   }
 
   /// Parameters for making an HTTP Request. Each of these parameters is an
@@ -176,6 +232,9 @@ interface types {
     /// This method consumes the `response-outparam` to ensure that it is
     /// called at most once. If it is never called, the implementation
     /// will respond with an error.
+    ///
+    /// The user may provide an `error` to `response` to allow the
+    /// implementation determine how to respond with an HTTP error response.
     set: static func(
       param: response-outparam,
       response: result<outgoing-response, error>,
@@ -260,16 +319,29 @@ interface types {
   resource outgoing-response {
 
     /// Construct an `outgoing-response`.
-    constructor(status-code: status-code, headers: borrow<headers>);
+    ///
+    /// * `status-code` is the HTTP Status Code for the Response.
+    /// * `headers` is the HTTP Headers for the Response.
+    constructor(status-code: status-code, headers: headers);
+
+    /// Get the HTTP Status Code for the Response.
+    status-code: func() -> status-code;
+    /// Set the HTTP Status Code for the Response.
+    set-status-code: func(status-code: status-code);
+
+    /// Get the headers associated with the Request.
+    ///
+    /// This headers resource is a child: it must be dropped before the parent
+    /// `outgoing-request` is dropped, or its ownership is transfered to
+    /// another component by e.g. `outgoing-handler.handle`.
+    headers: func() -> headers;
 
     /// Returns the resource corresponding to the outgoing Body for this Response.
     ///
     /// Returns success on the first call: the `outgoing-body` resource for
-    /// this `outgoing-response` can be retrieved at most once. Sunsequent
+    /// this `outgoing-response` can be retrieved at most once. Subsequent
     /// calls will return error.
-    ///
-    /// FIXME: rename this method to `body`.
-    write: func() -> result<outgoing-body>;
+    body: func() -> result<outgoing-body>;
   }
 
   /// Represents an outgoing HTTP Request or Response's Body.
diff --git a/src/commands/serve.rs b/src/commands/serve.rs
index e1b2d3625..762f73c8a 100644
--- a/src/commands/serve.rs
+++ b/src/commands/serve.rs
@@ -250,6 +250,8 @@ impl ServeCommand {
 
         let listener = tokio::net::TcpListener::bind(self.addr).await?;
 
+        eprintln!("Serving HTTP on http://{}/", listener.local_addr()?);
+
         let _epoch_thread = if let Some(timeout) = self.run.common.wasm.timeout {
             Some(EpochThread::spawn(timeout, engine.clone()))
         } else {
@@ -380,8 +382,11 @@ impl hyper::service::Service<Request> for ProxyHandler {
         });
 
         Box::pin(async move {
-            let resp = receiver.await.unwrap()?;
-            Ok(resp)
+            match receiver.await {
+                Ok(Ok(resp)) => Ok(resp),
+                Ok(Err(e)) => Err(e.into()),
+                Err(_) => bail!("guest never invoked `response-outparam::set` method"),
+            }
         })
     }
 }


Test patch:
diff --git a/crates/test-programs/src/bin/api_proxy.rs b/crates/test-programs/src/bin/api_proxy.rs
index 6798d4e30..ae75c1d3b 100644
--- a/crates/test-programs/src/bin/api_proxy.rs
+++ b/crates/test-programs/src/bin/api_proxy.rs
@@ -17,8 +17,8 @@ struct T;
 impl bindings::exports::wasi::http::incoming_handler::Guest for T {
     fn handle(_request: IncomingRequest, outparam: ResponseOutparam) {
         let hdrs = bindings::wasi::http::types::Headers::new(&[]);
-        let resp = bindings::wasi::http::types::OutgoingResponse::new(200, &hdrs);
-        let body = resp.write().expect("outgoing response");
+        let resp = bindings::wasi::http::types::OutgoingResponse::new(200, hdrs);
+        let body = resp.body().expect("outgoing response");
 
         bindings::wasi::http::types::ResponseOutparam::set(outparam, Ok(resp));
 
diff --git a/crates/test-programs/src/bin/api_proxy_streaming.rs b/crates/test-programs/src/bin/api_proxy_streaming.rs
index f4978ac17..e63223058 100644
--- a/crates/test-programs/src/bin/api_proxy_streaming.rs
+++ b/crates/test-programs/src/bin/api_proxy_streaming.rs
@@ -51,11 +51,11 @@ async fn handle_request(request: IncomingRequest, response_out: ResponseOutparam
 
             let response = OutgoingResponse::new(
                 200,
-                &Fields::new(&[("content-type".to_string(), b"text/plain".to_vec())]),
+                Fields::new(&[("content-type".to_string(), b"text/plain".to_vec())]),
             );
 
             let mut body =
-                executor::outgoing_body(response.write().expect("response should be writable"));
+                executor::outgoing_body(response.body().expect("response should be writable"));
 
             ResponseOutparam::set(response_out, Ok(response));
 
@@ -75,7 +75,7 @@ async fn handle_request(request: IncomingRequest, response_out: ResponseOutparam
         (Method::Post, Some("/echo")) => {
             let response = OutgoingResponse::new(
                 200,
-                &Fields::new(
+                Fields::new(
                     &headers
                         .into_iter()
                         .filter_map(|(k, v)| (k == "content-type").then_some((k, v)))
@@ -84,7 +84,7 @@ async fn handle_request(request: IncomingRequest, response_out: ResponseOutparam
             );
 
             let mut body =
-                executor::outgoing_body(response.write().expect("response should be writable"));
+                executor::outgoing_body(response.body().expect("response should be writable"));
 
             ResponseOutparam::set(response_out, Ok(response));
 
@@ -108,9 +108,9 @@ async fn handle_request(request: IncomingRequest, response_out: ResponseOutparam
         }
 
         _ => {
-            let response = OutgoingResponse::new(405, &Fields::new(&[]));
+            let response = OutgoingResponse::new(405, Fields::new(&[]));
 
-            let body = response.write().expect("response should be writable");
+            let body = response.body().expect("response should be writable");
 
             ResponseOutparam::set(response_out, Ok(response));
 
@@ -137,7 +137,7 @@ async fn hash(url: &Url) -> Result<String> {
                 String::new()
             }
         )),
-        &Fields::new(&[]),
+        Fields::new(&[]),
     );
 
     let response = executor::outgoing_request_send(request).await?;
diff --git a/crates/test-programs/src/bin/http_outbound_request_response_build.rs b/crates/test-programs/src/bin/http_outbound_request_response_build.rs
index b377b4a47..d9a29822d 100644
--- a/crates/test-programs/src/bin/http_outbound_request_response_build.rs
+++ b/crates/test-programs/src/bin/http_outbound_request_response_build.rs
@@ -12,9 +12,9 @@ fn main() {
             None,
             Some(&http_types::Scheme::Https),
             Some("www.example.com"),
-            &headers,
+            headers,
         );
-        let outgoing_body = request.write().unwrap();
+        let outgoing_body = request.body().unwrap();
         let request_body = outgoing_body.write().unwrap();
         request_body
             .blocking_write_and_flush("request-body".as_bytes())
@@ -25,8 +25,8 @@ fn main() {
             "Content-Type".to_string(),
             "application/text".to_string().into_bytes(),
         )]);
-        let response = http_types::OutgoingResponse::new(200, &headers);
-        let outgoing_body = response.write().unwrap();
+        let response = http_types::OutgoingResponse::new(200, headers);
+        let outgoing_body = response.body().unwrap();
         let response_body = outgoing_body.write().unwrap();
         response_body
             .blocking_write_and_flush("response-body".as_bytes())
diff --git a/crates/test-programs/src/http.rs b/crates/test-programs/src/http.rs
index e87347cb5..f065b45ee 100644
--- a/crates/test-programs/src/http.rs
+++ b/crates/test-programs/src/http.rs
@@ -58,11 +58,11 @@ pub fn request(
         Some(path_with_query),
         Some(&scheme),
         Some(authority),
-        &headers,
+        headers,
     );
 
     let outgoing_body = request
-        .write()
+        .body()
         .map_err(|_| anyhow!("outgoing request write failed"))?;
 
     if let Some(mut buf) = body {