Repository: bytecodealliance/wasmtime
Pull request #7641 (2023-12-06 00:02:25)

Backport the fields.has addition to the 16.x release branch
Backport #7625 to the 16.x release branch.

It doesn't seem like this needs an entry in the release notes, as it's just keeping up with the upstream wasi-http spec, but I'd be happy to add one.
<!--
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/crates/wasi-http/src/types_impl.rs b/crates/wasi-http/src/types_impl.rs
index 4ecb27481..74ee18c2f 100644
--- a/crates/wasi-http/src/types_impl.rs
+++ b/crates/wasi-http/src/types_impl.rs
@@ -145,20 +145,34 @@ impl<T: WasiHttpView> crate::bindings::http::types::HostFields for T {
         fields: Resource<HostFields>,
         name: String,
     ) -> wasmtime::Result<Vec<Vec<u8>>> {
+        let fields = get_fields(self.table(), &fields).context("[fields_get] getting fields")?;
+
         let header = match hyper::header::HeaderName::from_bytes(name.as_bytes()) {
             Ok(header) => header,
             Err(_) => return Ok(vec![]),
         };
 
-        let res = get_fields(self.table(), &fields)
-            .context("[fields_get] getting fields")?
-            .get_all(header)
+        if !fields.contains_key(&header) {
+            return Ok(vec![]);
+        }
+
+        let res = fields
+            .get_all(&header)
             .into_iter()
             .map(|val| val.as_bytes().to_owned())
             .collect();
         Ok(res)
     }
 
+    fn has(&mut self, fields: Resource<HostFields>, name: String) -> wasmtime::Result<bool> {
+        let fields = get_fields(self.table(), &fields).context("[fields_get] getting fields")?;
+
+        match hyper::header::HeaderName::from_bytes(name.as_bytes()) {
+            Ok(header) => Ok(fields.contains_key(&header)),
+            Err(_) => Ok(false),
+        }
+    }
+
     fn set(
         &mut self,
         fields: Resource<HostFields>,
diff --git a/crates/wasi-http/wit/deps/http/types.wit b/crates/wasi-http/wit/deps/http/types.wit
index c42f841bf..0f698e769 100644
--- a/crates/wasi-http/wit/deps/http/types.wit
+++ b/crates/wasi-http/wit/deps/http/types.wit
@@ -169,9 +169,16 @@ interface types {
       entries: list<tuple<field-key,field-value>>
     ) -> result<fields, header-error>;
 
-    /// Get all of the values corresponding to a key.
+    /// Get all of the values corresponding to a key. If the key is not present
+    /// in this `fields`, an empty list is returned. However, if the key is
+    /// present but empty, this is represented by a list with one or more
+    /// empty field-values present.
     get: func(name: field-key) -> list<field-value>;
 
+    /// Returns `true` when the key is present in this `fields`. If the key is
+    /// syntactically invalid, `false` is returned.
+    has: func(name: field-key) -> bool;
+
     /// Set all of the values for a key. Clears any existing values for that
     /// key, if they have been set.
     ///
@@ -191,7 +198,7 @@ interface types {
     append: func(name: field-key, value: field-value) -> result<_, header-error>;
 
     /// Retrieve the full set of keys and values in the Fields. Like the
-    /// constructor, the list represents each key-value pair. 
+    /// constructor, the list represents each key-value pair.
     ///
     /// The outer list represents each key-value pair in the Fields. Keys
     /// which have multiple values are represented by multiple entries in this
@@ -308,8 +315,9 @@ interface types {
     headers: func() -> headers;
   }
 
-  /// Parameters for making an HTTP Request. Each of these parameters is an
-  /// optional timeout, applicable to the transport layer of the HTTP protocol.
+  /// Parameters for making an HTTP Request. Each of these parameters is
+  /// currently an optional timeout applicable to the transport layer of the
+  /// HTTP protocol.
   ///
   /// These timeouts are separate from any the user may use to bound a
   /// blocking call to `wasi:io/poll.poll`.
diff --git a/crates/wasi/wit/deps/http/types.wit b/crates/wasi/wit/deps/http/types.wit
index c42f841bf..0f698e769 100644
--- a/crates/wasi/wit/deps/http/types.wit
+++ b/crates/wasi/wit/deps/http/types.wit
@@ -169,9 +169,16 @@ interface types {
       entries: list<tuple<field-key,field-value>>
     ) -> result<fields, header-error>;
 
-    /// Get all of the values corresponding to a key.
+    /// Get all of the values corresponding to a key. If the key is not present
+    /// in this `fields`, an empty list is returned. However, if the key is
+    /// present but empty, this is represented by a list with one or more
+    /// empty field-values present.
     get: func(name: field-key) -> list<field-value>;
 
+    /// Returns `true` when the key is present in this `fields`. If the key is
+    /// syntactically invalid, `false` is returned.
+    has: func(name: field-key) -> bool;
+
     /// Set all of the values for a key. Clears any existing values for that
     /// key, if they have been set.
     ///
@@ -191,7 +198,7 @@ interface types {
     append: func(name: field-key, value: field-value) -> result<_, header-error>;
 
     /// Retrieve the full set of keys and values in the Fields. Like the
-    /// constructor, the list represents each key-value pair. 
+    /// constructor, the list represents each key-value pair.
     ///
     /// The outer list represents each key-value pair in the Fields. Keys
     /// which have multiple values are represented by multiple entries in this
@@ -308,8 +315,9 @@ interface types {
     headers: func() -> headers;
   }
 
-  /// Parameters for making an HTTP Request. Each of these parameters is an
-  /// optional timeout, applicable to the transport layer of the HTTP protocol.
+  /// Parameters for making an HTTP Request. Each of these parameters is
+  /// currently an optional timeout applicable to the transport layer of the
+  /// HTTP protocol.
   ///
   /// These timeouts are separate from any the user may use to bound a
   /// blocking call to `wasi:io/poll.poll`.


Test patch:
diff --git a/crates/test-programs/src/bin/api_proxy.rs b/crates/test-programs/src/bin/api_proxy.rs
index 33114699b..a0b26cfd7 100644
--- a/crates/test-programs/src/bin/api_proxy.rs
+++ b/crates/test-programs/src/bin/api_proxy.rs
@@ -24,7 +24,7 @@ impl bindings::exports::wasi::http::incoming_handler::Guest for T {
         let req_hdrs = request.headers();
 
         assert!(
-            req_hdrs.get(&header).is_empty(),
+            !req_hdrs.has(&header),
             "forbidden `custom-forbidden-header` found in request"
         );
 
@@ -32,7 +32,7 @@ impl bindings::exports::wasi::http::incoming_handler::Guest for T {
         assert!(req_hdrs.append(&header, &b"no".to_vec()).is_err());
 
         assert!(
-            req_hdrs.get(&header).is_empty(),
+            !req_hdrs.has(&header),
             "append of forbidden header succeeded"
         );